From 736578fb4e2db009d096ebd7a84ae546ac2ace95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:13:41 +0300 Subject: [PATCH 1/7] Prefer use of `resourceUuid` index over `resourceType` when selecting from `resourceentity` table (#2739) * Remove resourceType when searching with resourceUuid * Remove inOrder assertion for search results not sorted * Fix NumberSearchParameterizedTest with correct query * Add SearchResultCorrespondenceUnorderedIncludeRevInclude Switching from resourceType multi-column index to resourceUuid, the order of results in include/revInclude is no longer predictable since the resourceUuids are randomly generated, also saved in the db as blob and hence ordered by byte representation of the resourceUuid * Refactor filterStatement to include ifBlank * Revert add Unit type in DatabaseImplTest test methods --------- Co-authored-by: Benjamin Mwalimu --- .../android/fhir/db/impl/DatabaseImplTest.kt | 382 ++++++++++-------- .../google/android/fhir/search/MoreSearch.kt | 61 +-- .../android/fhir/search/NestedSearch.kt | 2 +- .../search/NumberSearchParameterizedTest.kt | 12 +- .../google/android/fhir/search/SearchTest.kt | 278 ++++--------- 5 files changed, 323 insertions(+), 412 deletions(-) diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt index 1807b4b2d6..e8e7876358 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt @@ -2633,92 +2633,93 @@ class DatabaseImplTest { } @Test - fun search_filter_param_values_disjunction_covid_immunization_records() = runBlocking { - val resources = - listOf( - Immunization().apply { - id = "immunization-1" - vaccineCode = - CodeableConcept( - Coding( - "http://id.who.int/icd11/mms", - "XM1NL1", - "COVID-19 vaccine, inactivated virus", - ), - ) - status = Immunization.ImmunizationStatus.COMPLETED - }, - Immunization().apply { - id = "immunization-2" - vaccineCode = - CodeableConcept( - Coding( - "http://id.who.int/icd11/mms", - "XM5DF6", - "COVID-19 vaccine, live attenuated virus", - ), - ) - status = Immunization.ImmunizationStatus.COMPLETED - }, - Immunization().apply { - id = "immunization-3" - vaccineCode = - CodeableConcept( - Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based"), - ) - status = Immunization.ImmunizationStatus.COMPLETED - }, - Immunization().apply { - id = "immunization-4" - vaccineCode = - CodeableConcept( - Coding( - "http://hl7.org/fhir/sid/cvx", - "140", - "Influenza, seasonal, injectable, preservative free", - ), - ) - status = Immunization.ImmunizationStatus.COMPLETED - }, - ) + fun search_filter_param_values_disjunction_covid_immunization_records() { + runBlocking { + val resources = + listOf( + Immunization().apply { + id = "immunization-1" + vaccineCode = + CodeableConcept( + Coding( + "http://id.who.int/icd11/mms", + "XM1NL1", + "COVID-19 vaccine, inactivated virus", + ), + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-2" + vaccineCode = + CodeableConcept( + Coding( + "http://id.who.int/icd11/mms", + "XM5DF6", + "COVID-19 vaccine, live attenuated virus", + ), + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-3" + vaccineCode = + CodeableConcept( + Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based"), + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-4" + vaccineCode = + CodeableConcept( + Coding( + "http://hl7.org/fhir/sid/cvx", + "140", + "Influenza, seasonal, injectable, preservative free", + ), + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + ) - database.insert(*resources.toTypedArray()) + database.insert(*resources.toTypedArray()) - val result = - database.search( - Search(ResourceType.Immunization) - .apply { - filter( - Immunization.VACCINE_CODE, - { - value = - of( - Coding( - "http://id.who.int/icd11/mms", - "XM1NL1", - "COVID-19 vaccine, inactivated virus", - ), - ) - }, - { - value = - of( - Coding( - "http://id.who.int/icd11/mms", - "XM5DF6", - "COVID-19 vaccine, inactivated virus", - ), - ) - }, - operation = Operation.OR, - ) - } - .getQuery(), - ) + val result = + database.search( + Search(ResourceType.Immunization) + .apply { + filter( + Immunization.VACCINE_CODE, + { + value = + of( + Coding( + "http://id.who.int/icd11/mms", + "XM1NL1", + "COVID-19 vaccine, inactivated virus", + ), + ) + }, + { + value = + of( + Coding( + "http://id.who.int/icd11/mms", + "XM5DF6", + "COVID-19 vaccine, inactivated virus", + ), + ) + }, + operation = Operation.OR, + ) + } + .getQuery(), + ) - assertThat(result.map { it.resource.vaccineCode.codingFirstRep.code }) - .containsExactly("XM1NL1", "XM5DF6") - .inOrder() + assertThat(result.map { it.resource.vaccineCode.codingFirstRep.code }) + .containsExactly("XM1NL1", "XM5DF6") + } } @Test @@ -2797,95 +2798,96 @@ class DatabaseImplTest { } @Test - fun test_search_multiple_param_conjunction_with_multiple_values_disjunction() = runBlocking { - val resources = - listOf( - Patient().apply { - id = "patient-01" - addName( - HumanName().apply { - addGiven("John") - family = "Doe" - }, - ) - }, - Patient().apply { - id = "patient-02" - addName( - HumanName().apply { - addGiven("Jane") - family = "Doe" - }, - ) - }, - Patient().apply { - id = "patient-03" - addName( - HumanName().apply { - addGiven("John") - family = "Roe" - }, - ) - }, - Patient().apply { - id = "patient-04" - addName( - HumanName().apply { - addGiven("Jane") - family = "Roe" - }, - ) - }, - Patient().apply { - id = "patient-05" - addName( - HumanName().apply { - addGiven("Rocky") - family = "Balboa" - }, - ) - }, - ) - database.insert(*resources.toTypedArray()) - - val result = - database.search( - Search(ResourceType.Patient) - .apply { - filter( - Patient.GIVEN, - { - value = "John" - modifier = StringFilterModifier.MATCHES_EXACTLY + fun test_search_multiple_param_conjunction_with_multiple_values_disjunction() { + runBlocking { + val resources = + listOf( + Patient().apply { + id = "patient-01" + addName( + HumanName().apply { + addGiven("John") + family = "Doe" }, - { - value = "Jane" - modifier = StringFilterModifier.MATCHES_EXACTLY + ) + }, + Patient().apply { + id = "patient-02" + addName( + HumanName().apply { + addGiven("Jane") + family = "Doe" }, - operation = Operation.OR, ) - - filter( - Patient.FAMILY, - { - value = "Doe" - modifier = StringFilterModifier.MATCHES_EXACTLY + }, + Patient().apply { + id = "patient-03" + addName( + HumanName().apply { + addGiven("John") + family = "Roe" }, - { - value = "Roe" - modifier = StringFilterModifier.MATCHES_EXACTLY + ) + }, + Patient().apply { + id = "patient-04" + addName( + HumanName().apply { + addGiven("Jane") + family = "Roe" + }, + ) + }, + Patient().apply { + id = "patient-05" + addName( + HumanName().apply { + addGiven("Rocky") + family = "Balboa" }, - operation = Operation.OR, ) + }, + ) + database.insert(*resources.toTypedArray()) - operation = Operation.AND - } - .getQuery(), - ) + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "John" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + { + value = "Jane" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + operation = Operation.OR, + ) - assertThat(result.map { it.resource.nameFirstRep.nameAsSingleString }) - .containsExactly("John Doe", "Jane Doe", "John Roe", "Jane Roe") - .inOrder() + filter( + Patient.FAMILY, + { + value = "Doe" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + { + value = "Roe" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + operation = Operation.OR, + ) + + operation = Operation.AND + } + .getQuery(), + ) + + assertThat(result.map { it.resource.nameFirstRep.nameAsSingleString }) + .containsExactly("John Doe", "Jane Doe", "John Roe", "Jane Roe") + } } @Test @@ -3137,7 +3139,6 @@ class DatabaseImplTest { revIncluded = null, ), ) - .inOrder() } @Test @@ -3230,7 +3231,6 @@ class DatabaseImplTest { mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)), ), ) - .inOrder() } @Test @@ -3534,7 +3534,7 @@ class DatabaseImplTest { .execute(database) assertThat(result) - .comparingElementsUsing(SearchResultCorrespondence) + .comparingElementsUsing(SearchResultCorrespondenceUnorderedIncludeRevInclude) .displayingDiffsPairedBy { it.resource.logicalId } .containsExactly( SearchResult( @@ -3576,7 +3576,6 @@ class DatabaseImplTest { ), ), ) - .inOrder() } @Test @@ -3695,11 +3694,10 @@ class DatabaseImplTest { revIncluded = null, ), ) - .inOrder() } @Test - fun search_patient_and_revinclude_person_should_map_common_person_to_all_matching_patients() = + fun search_patient_and_revinclude_person_should_map_common_person_to_all_matching_patients() { runBlocking { val person1 = Person().apply { @@ -3813,8 +3811,8 @@ class DatabaseImplTest { mapOf(Pair(ResourceType.Person, Person.LINK.paramName) to listOf(person2, person3)), ), ) - .inOrder() } + } @Test fun search_patient_and_revInclude_encounters_sorted_by_date_descending(): Unit = runBlocking { @@ -5239,6 +5237,18 @@ class DatabaseImplTest { ) .formattingDiffsUsing(::formatDiff) + /** + * [Correspondence] to provide a custom [equalityCheck] for the [SearchResult]s whereby + * [SearchResult.included] and [SearchResult.revIncluded] may not be in the correct order + */ + val SearchResultCorrespondenceUnorderedIncludeRevInclude: + Correspondence, SearchResult> = + Correspondence.from, SearchResult>( + ::equalityCheckUnordered, + "is shallow equals (by logical id comparison) to the ", + ) + .formattingDiffsUsing(::formatDiff) + private fun equalityCheck( actual: SearchResult, expected: SearchResult, @@ -5248,6 +5258,15 @@ class DatabaseImplTest { equalsShallow(actual.revIncluded, expected.revIncluded) } + private fun equalityCheckUnordered( + actual: SearchResult, + expected: SearchResult, + ): Boolean { + return equalsShallow(actual.resource, expected.resource) && + equalsShallow(actual.included, expected.included, inOrder = false) && + equalsShallow(actual.revIncluded, expected.revIncluded, inOrder = false) + } + private fun equalsShallow(first: Resource, second: Resource) = first.resourceType == second.resourceType && first.logicalId == second.logicalId @@ -5255,13 +5274,24 @@ class DatabaseImplTest { first.size == second.size && first.asSequence().zip(second.asSequence()).all { (x, y) -> equalsShallow(x, y) } + private fun resourceTypeAndIdEqualUnordered(first: List, second: List) = + first.size == second.size && + first.map { it.resourceType to it.logicalId }.toSet() == + second.map { it.resourceType to it.logicalId }.toSet() + private fun equalsShallow( first: Map>?, second: Map>?, + inOrder: Boolean = true, ) = if (first != null && second != null && first.size == second.size) { first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> - x.key == y.key && equalsShallow(x.value, y.value) + x.key == y.key && + if (inOrder) { + equalsShallow(x.value, y.value) + } else { + resourceTypeAndIdEqualUnordered(x.value, y.value) + } } } else { first?.size == second?.size @@ -5271,10 +5301,16 @@ class DatabaseImplTest { private fun equalsShallow( first: Map, List>?, second: Map, List>?, + inOrder: Boolean = true, ) = if (first != null && second != null && first.size == second.size) { first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> - x.key == y.key && equalsShallow(x.value, y.value) + x.key == y.key && + if (inOrder) { + equalsShallow(x.value, y.value) + } else { + resourceTypeAndIdEqualUnordered(x.value, y.value) + } } } else { first?.size == second?.size diff --git a/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt b/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt index e12919b8d7..daa577b68f 100644 --- a/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt @@ -116,7 +116,6 @@ internal fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { args.add(resourceToInclude.name) args.add(param.paramName) args.addAll(includeIds) - args.add(resourceToInclude.name) var filterQuery = "" val filters = search.getFilterQueries() @@ -136,6 +135,7 @@ internal fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { } } } + if (filters.isEmpty()) args.add(resourceToInclude.name) return filterQuery } @@ -151,8 +151,8 @@ internal fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { JOIN ReferenceIndexEntity rie ON re.resourceUuid = rie.resourceUuid ${join.query} - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN ($uuidsString) AND re.resourceType = ? - ${if (filterQuery.isNotEmpty()) "AND re.resourceUuid IN ($filterQuery)" else ""} + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN ($uuidsString) + ${if (filterQuery.isNotBlank()) "AND re.resourceUuid IN ($filterQuery)" else "AND re.resourceType = ?"} $order """ .trimIndent() @@ -179,7 +179,6 @@ internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { args.add(baseResourceType.name) args.add(param.paramName) args.addAll(includeIds.map { convertUUIDToByte(it) }) - args.add(resourceToInclude.name) var filterQuery = "" val filters = search.getFilterQueries() @@ -199,6 +198,7 @@ internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { } } } + if (filters.isEmpty()) args.add(resourceToInclude.name) return filterQuery } @@ -214,8 +214,8 @@ internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { JOIN ReferenceIndexEntity rie ON re.resourceType||"/"||re.resourceId = rie.index_value ${join.query} - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN ($uuidsString) AND re.resourceType = ? - ${if (filterQuery.isNotEmpty()) "AND re.resourceUuid IN ($filterQuery)" else ""} + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN ($uuidsString) + ${if (filterQuery.isNotBlank()) "AND re.resourceUuid IN ($filterQuery)" else "AND re.resourceType = ?"} $order """ .trimIndent() @@ -363,21 +363,19 @@ internal fun Search.getQuery( val sortOrderStatement = order val sortArgs = join.args - var filterStatement = "" - val filterArgs = mutableListOf() val filterQuery = getFilterQueries() - filterQuery.forEachIndexed { i, it -> - filterStatement += + val filterQueryStatement = + filterQuery.joinToString(separator = "${operation.logicalOperator} ") { // spotless:off - """ - ${if (i == 0) "AND a.resourceUuid IN (" else "a.resourceUuid IN ("} + """ + a.resourceUuid IN ( ${it.query} ) - ${if (i != filterQuery.lastIndex) "${operation.logicalOperator} " else ""} + """.trimIndent() // spotless:on - filterArgs.addAll(it.args) - } + } + val filterQueryArgs = filterQuery.flatMap { it.args } var limitStatement = "" val limitArgs = mutableListOf() @@ -390,10 +388,20 @@ internal fun Search.getQuery( } } - nestedSearches.nestedQuery(type, operation)?.let { - filterStatement += it.query - filterArgs.addAll(it.args) - } + val nestedFilterQuery = nestedSearches.nestedQuery(type, operation) + val nestedQueryFilterStatement = nestedFilterQuery?.query ?: "" + val nestedQueryFilterArgs = nestedFilterQuery?.args ?: emptyList() + + // Combines filter statements derived from filter queries and nested queries, that use the + // resourceUuid field, + // and defaults to filter statement with the resourceType field when blank + val filterStatement = + listOf(filterQueryStatement, nestedQueryFilterStatement) + .filter { it.isNotBlank() } + .joinToString(separator = " AND ") + .ifBlank { "a.resourceType = ?" } + val filterArgs = (filterQueryArgs + nestedQueryFilterArgs).ifEmpty { listOf(type.name) } + val whereArgs = mutableListOf() val nestedArgs = mutableListOf() val query = @@ -404,8 +412,7 @@ internal fun Search.getQuery( SELECT COUNT(*) FROM ResourceEntity a $sortJoinStatement - WHERE a.resourceType = ? - $filterStatement + WHERE $filterStatement $sortOrderStatement $limitStatement """ @@ -423,8 +430,7 @@ internal fun Search.getQuery( SELECT substr(a.index_value, $start) FROM ReferenceIndexEntity a $sortJoinStatement - WHERE a.resourceType = ? AND a.index_name = ? - $filterStatement + WHERE a.index_name = ? AND $filterStatement $sortOrderStatement $limitStatement) """ @@ -436,8 +442,7 @@ internal fun Search.getQuery( SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a $sortJoinStatement - WHERE a.resourceType = ? - $filterStatement + WHERE $filterStatement $sortOrderStatement $limitStatement """ @@ -446,7 +451,11 @@ internal fun Search.getQuery( .split("\n") .filter { it.isNotBlank() } .joinToString("\n") { it.trim() } - return SearchQuery(query, nestedArgs + sortArgs + type.name + whereArgs + filterArgs + limitArgs) + + return SearchQuery( + query, + nestedArgs + sortArgs + whereArgs + filterArgs + limitArgs, + ) } private val Order?.sqlString: String diff --git a/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt b/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt index 4818680ec5..1f39a00c32 100644 --- a/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt @@ -219,7 +219,7 @@ internal fun List.nestedQuery( SearchQuery( query = searchQueries.joinToString( - prefix = "AND a.resourceUuid IN ", + prefix = "a.resourceUuid IN ", separator = " ${operation.logicalOperator} a.resourceUuid IN", ) { searchQuery -> "(\n${searchQuery.query}\n) " diff --git a/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt b/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt index 7dcb4e177d..a7f597bedb 100644 --- a/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt +++ b/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt @@ -53,8 +53,7 @@ class NumberSearchParameterizedTest( """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM NumberIndexEntity """ .trimIndent() @@ -86,7 +85,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, lowerBound.toDouble(), @@ -122,7 +120,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, lowerBound.toDouble(), @@ -158,7 +155,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -193,7 +189,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -228,7 +223,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -262,7 +256,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -317,7 +310,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -372,7 +364,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, num.toDouble(), @@ -407,7 +398,6 @@ class NumberSearchParameterizedTest( assertThat(search.args) .isEqualTo( listOf( - ResourceType.RiskAssessment.name, ResourceType.RiskAssessment.name, RiskAssessment.PROBABILITY.paramName, 0.09, diff --git a/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt b/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt index 6fea58fc2a..2ddb427a55 100644 --- a/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt +++ b/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt @@ -156,8 +156,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from BETWEEN ? AND ? AND index_to BETWEEN ? AND ?) ) @@ -178,7 +177,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, diffStart, @@ -209,8 +207,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from > ? ) @@ -221,7 +218,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.epochDay, @@ -249,8 +245,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to < ? ) @@ -261,7 +256,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.epochDay, @@ -289,8 +283,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from NOT BETWEEN ? AND ? OR index_to NOT BETWEEN ? AND ?) ) @@ -301,7 +294,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateType("2013-03-14").value.epochDay, @@ -324,8 +316,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from BETWEEN ? AND ? AND index_to BETWEEN ? AND ?) ) @@ -336,7 +327,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateType("2013-03-14").value.epochDay, @@ -367,8 +357,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to > ? ) @@ -379,7 +368,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.epochDay, @@ -407,8 +395,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to >= ? ) @@ -419,7 +406,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.epochDay, @@ -447,8 +433,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from < ? ) @@ -459,7 +444,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateType("2013-03-14").value.epochDay, @@ -487,8 +471,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from <= ? ) @@ -499,7 +482,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateType("2013-03-14").value.epochDay, @@ -532,8 +514,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from BETWEEN ? AND ? AND index_to BETWEEN ? AND ?) ) @@ -555,7 +536,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, diffStart, @@ -586,8 +566,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from > ? ) @@ -598,7 +577,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-15").value.time - 1, @@ -626,8 +604,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to < ? ) @@ -638,7 +615,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.time, @@ -666,8 +642,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from NOT BETWEEN ? AND ? OR index_to NOT BETWEEN ? AND ?) ) @@ -678,7 +653,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.time, @@ -709,8 +683,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_from BETWEEN ? AND ? AND index_to BETWEEN ? AND ?) ) @@ -721,7 +694,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.time, @@ -752,8 +724,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to > ? ) @@ -764,7 +735,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-15").value.time - 1, @@ -792,8 +762,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_to >= ? ) @@ -804,7 +773,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.time, @@ -832,8 +800,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from < ? ) @@ -844,7 +811,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-14").value.time, @@ -872,8 +838,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM DateTimeIndexEntity WHERE resourceType = ? AND index_name = ? AND index_from <= ? ) @@ -884,7 +849,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.BIRTHDATE.paramName, DateTimeType("2013-03-15").value.time - 1, @@ -904,8 +868,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value LIKE ? || '%' COLLATE NOCASE ) @@ -914,7 +877,6 @@ class SearchTest { ) assertThat(query.args) .containsExactly( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.ADDRESS.paramName, "someValue", @@ -941,8 +903,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -951,7 +912,6 @@ class SearchTest { ) assertThat(query.args) .containsExactly( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.ADDRESS.paramName, "someValue", @@ -978,8 +938,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value LIKE '%' || ? || '%' COLLATE NOCASE ) @@ -988,7 +947,6 @@ class SearchTest { ) assertThat(query.args) .containsExactly( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.ADDRESS.paramName, "someValue", @@ -1015,8 +973,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1026,7 +983,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.GENDER.paramName, "male", @@ -1055,8 +1011,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1066,7 +1021,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Immunization.name, ResourceType.Immunization.name, Immunization.VACCINE_CODE.paramName, "260385009", @@ -1090,8 +1044,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1101,7 +1054,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.IDENTIFIER.paramName, "12345", @@ -1135,8 +1087,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1146,7 +1097,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.TELECOM.paramName, "test@gmail.com", @@ -1179,8 +1129,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1190,7 +1139,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.TELECOM.paramName, "test@gmail.com", @@ -1209,8 +1157,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1220,7 +1167,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.GENDER.paramName, "male", @@ -1238,8 +1184,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1249,7 +1194,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.ACTIVE.paramName, "true", @@ -1274,8 +1218,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1285,7 +1228,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.IDENTIFIER.paramName, "16009886-bd57-11eb-8529-0242ac130003", @@ -1305,8 +1247,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1316,7 +1257,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.PHONE.paramName, "+14845219791", @@ -1344,8 +1284,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_code = ? AND index_value >= ? AND index_value < ?) ) @@ -1355,7 +1294,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, "g", @@ -1385,8 +1323,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_code = ? AND index_value < ?) ) @@ -1396,7 +1333,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, "g", @@ -1425,8 +1361,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_system = ? AND index_value <= ?) ) @@ -1436,7 +1371,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, "http://unitsofmeasure.org", @@ -1465,8 +1399,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_system = ? AND index_value > ?) ) @@ -1476,7 +1409,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, "http://unitsofmeasure.org", @@ -1504,8 +1436,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value >= ? ) @@ -1515,7 +1446,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, BigDecimal("5.403").toDouble(), @@ -1542,8 +1472,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value < ? OR index_value >= ?) ) @@ -1553,7 +1482,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, BigDecimal("5.4025").toDouble(), @@ -1581,8 +1509,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value > ? ) @@ -1592,7 +1519,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, BigDecimal("5.403").toDouble(), @@ -1619,8 +1545,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value < ? ) @@ -1630,7 +1555,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, BigDecimal("5.403").toDouble(), @@ -1659,8 +1583,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM QuantityIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_system = ? AND index_code = ? AND index_value >= ? AND index_value < ?) ) @@ -1670,7 +1593,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOfNotNull( - ResourceType.Observation.name, ResourceType.Observation.name, Observation.VALUE_QUANTITY.paramName, "http://unitsofmeasure.org", @@ -1691,8 +1613,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM UriIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1701,7 +1622,6 @@ class SearchTest { ) assertThat(query.args) .containsExactly( - ResourceType.Library.name, ResourceType.Library.name, Library.URL.paramName, "someValue", @@ -1794,8 +1714,7 @@ class SearchTest { FROM ResourceEntity a LEFT JOIN StringIndexEntity b ON a.resourceUuid = b.resourceUuid AND b.index_name = ? - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value LIKE ? || '%' COLLATE NOCASE ) @@ -1811,7 +1730,6 @@ class SearchTest { listOf( Patient.GIVEN.paramName, ResourceType.Patient.name, - ResourceType.Patient.name, Patient.FAMILY.paramName, "Jones", 10, @@ -1839,15 +1757,13 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceId IN ( SELECT substr(a.index_value, 9) FROM ReferenceIndexEntity a - WHERE a.resourceType = ? AND a.index_name = ? - AND a.resourceUuid IN ( + WHERE a.index_name = ? AND a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1861,8 +1777,6 @@ class SearchTest { .isEqualTo( listOf( ResourceType.Patient.name, - ResourceType.Patient.name, - ResourceType.Condition.name, Condition.SUBJECT.paramName, ResourceType.Condition.name, Condition.CODE.paramName, @@ -1915,8 +1829,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -1926,8 +1839,7 @@ class SearchTest { WHERE a.resourceType = ? AND a.resourceId IN ( SELECT substr(a.index_value, 9) FROM ReferenceIndexEntity a - WHERE a.resourceType = ? AND a.index_name = ? - AND a.resourceUuid IN ( + WHERE a.index_name = ? AND a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -1944,12 +1856,10 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - ResourceType.Patient.name, ResourceType.Patient.name, Patient.ADDRESS_COUNTRY.paramName, "IN", ResourceType.Patient.name, - ResourceType.Immunization.name, Immunization.PATIENT.paramName, ResourceType.Immunization.name, Immunization.VACCINE_CODE.paramName, @@ -1964,7 +1874,7 @@ class SearchTest { } @Test - fun practitioner_has_patient_has_condition_diabetes_and_hypertension() { + fun search_has_patient_has_condition_diabetes_and_hypertension() { val query = Search(ResourceType.Patient) .apply { @@ -1988,15 +1898,13 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceId IN ( SELECT substr(a.index_value, 9) FROM ReferenceIndexEntity a - WHERE a.resourceType = ? AND a.index_name = ? - AND a.resourceUuid IN ( + WHERE a.index_name = ? AND a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -2007,8 +1915,7 @@ class SearchTest { WHERE a.resourceType = ? AND a.resourceId IN ( SELECT substr(a.index_value, 9) FROM ReferenceIndexEntity a - WHERE a.resourceType = ? AND a.index_name = ? - AND a.resourceUuid IN ( + WHERE a.index_name = ? AND a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -2022,15 +1929,12 @@ class SearchTest { .isEqualTo( listOf( ResourceType.Patient.name, - ResourceType.Patient.name, - ResourceType.Condition.name, Condition.SUBJECT.paramName, ResourceType.Condition.name, Condition.CODE.paramName, "44054006", "http://snomed.info/sct", ResourceType.Patient.name, - ResourceType.Condition.name, Condition.SUBJECT.paramName, ResourceType.Condition.name, Condition.CODE.paramName, @@ -2111,8 +2015,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? OR index_value = ?) ) @@ -2120,7 +2023,7 @@ class SearchTest { .trimIndent(), ) - assertThat(query.args).isEqualTo(listOf("Patient", "Patient", "given", "John", "Jane")) + assertThat(query.args).isEqualTo(listOf("Patient", "given", "John", "Jane")) } @Test @@ -2152,8 +2055,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -2165,8 +2067,7 @@ class SearchTest { .trimIndent(), ) - assertThat(query.args) - .isEqualTo(listOf("Patient", "Patient", "given", "John", "Patient", "given", "Jane")) + assertThat(query.args).isEqualTo(listOf("Patient", "given", "John", "Patient", "given", "Jane")) } // Test for https://github.com/google/android-fhir/issues/903 @@ -2185,8 +2086,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM StringIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value LIKE ? || '%' COLLATE NOCASE ) @@ -2199,7 +2099,7 @@ class SearchTest { ) assertThat(query.args) - .isEqualTo(listOf("Patient", "Patient", "given", "John", "Patient", "family", "Doe", "Roe")) + .isEqualTo(listOf("Patient", "given", "John", "Patient", "family", "Doe", "Roe")) } @Test @@ -2216,8 +2116,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -2225,8 +2124,7 @@ class SearchTest { .trimIndent(), ) - assertThat(query.args) - .isEqualTo(listOf("Condition", "Condition", "clinical-status", "test-code")) + assertThat(query.args).isEqualTo(listOf("Condition", "clinical-status", "test-code")) } @Test @@ -2254,8 +2152,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) ) @@ -2266,7 +2163,6 @@ class SearchTest { assertThat(query.args) .isEqualTo( listOf( - "Condition", "Condition", "clinical-status", "test-code", @@ -2295,7 +2191,8 @@ class SearchTest { FROM ResourceEntity re JOIN ReferenceIndexEntity rie ON re.resourceType||"/"||re.resourceId = rie.index_value - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) + AND re.resourceType = ? ) """ .trimIndent(), @@ -2337,7 +2234,7 @@ class SearchTest { FROM ResourceEntity re JOIN ReferenceIndexEntity rie ON re.resourceType||"/"||re.resourceId = rie.index_value - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? @@ -2355,7 +2252,6 @@ class SearchTest { convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), "Practitioner", - "Practitioner", "active", "true", ) @@ -2389,7 +2285,7 @@ class SearchTest { ON re.resourceType||"/"||re.resourceId = rie.index_value LEFT JOIN StringIndexEntity b ON re.resourceUuid = b.resourceUuid AND b.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? @@ -2411,7 +2307,6 @@ class SearchTest { convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), "Practitioner", - "Practitioner", "active", "true", ) @@ -2450,7 +2345,7 @@ class SearchTest { ON re.resourceType||"/"||re.resourceId = rie.index_value LEFT JOIN StringIndexEntity b ON re.resourceUuid = b.resourceUuid AND b.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? @@ -2467,7 +2362,7 @@ class SearchTest { ON re.resourceType||"/"||re.resourceId = rie.index_value LEFT JOIN StringIndexEntity b ON re.resourceUuid = b.resourceUuid AND b.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? @@ -2489,7 +2384,6 @@ class SearchTest { convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), "Practitioner", - "Practitioner", "active", "true", "name", @@ -2498,7 +2392,6 @@ class SearchTest { convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), "Organization", - "Organization", "active", "true", ) @@ -2520,7 +2413,8 @@ class SearchTest { FROM ResourceEntity re JOIN ReferenceIndexEntity rie ON re.resourceUuid = rie.resourceUuid - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) + AND re.resourceType = ? ) """ .trimIndent(), @@ -2558,7 +2452,7 @@ class SearchTest { FROM ResourceEntity re JOIN ReferenceIndexEntity rie ON re.resourceUuid = rie.resourceUuid - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) @@ -2575,7 +2469,6 @@ class SearchTest { "Patient/pa01", "Patient/pa02", "Condition", - "Condition", "code", "44054006", "http://snomed.info/sct", @@ -2615,7 +2508,7 @@ class SearchTest { ON re.resourceUuid = b.resourceUuid AND b.index_name = ? LEFT JOIN DateTimeIndexEntity c ON re.resourceUuid = c.resourceUuid AND c.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) @@ -2637,7 +2530,6 @@ class SearchTest { "Patient/pa01", "Patient/pa02", "Condition", - "Condition", "code", "44054006", "http://snomed.info/sct", @@ -2694,7 +2586,7 @@ class SearchTest { ON re.resourceUuid = b.resourceUuid AND b.index_name = ? LEFT JOIN DateTimeIndexEntity c ON re.resourceUuid = c.resourceUuid AND c.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) @@ -2713,7 +2605,7 @@ class SearchTest { ON re.resourceUuid = b.resourceUuid AND b.index_name = ? LEFT JOIN DateTimeIndexEntity c ON re.resourceUuid = c.resourceUuid AND c.index_name = ? - WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceUuid IN ( SELECT resourceUuid FROM TokenIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) @@ -2735,7 +2627,6 @@ class SearchTest { "Patient/pa01", "Patient/pa02", "Encounter", - "Encounter", "status", "arrived", "http://hl7.org/fhir/encounter-status", @@ -2746,7 +2637,6 @@ class SearchTest { "Patient/pa01", "Patient/pa02", "Condition", - "Condition", "code", "44054006", "http://snomed.info/sct", @@ -2772,8 +2662,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND (((((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR (((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))))) OR ((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))))) OR (((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))))) OR ((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))))))) OR ((((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR (((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))))) OR ((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))))) OR (((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))))) OR ((((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))) OR (((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))))) OR ((((index_value = ? OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))) OR ((((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) OR (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)))))))))) ) @@ -2783,7 +2672,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", *patientIdReferenceList.toTypedArray(), @@ -2801,8 +2689,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? ) @@ -2812,7 +2699,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", ) @@ -2832,8 +2718,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND index_value = ? ) @@ -2843,7 +2728,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", "Patient/patient-0", @@ -2866,8 +2750,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? OR index_value = ?) ) @@ -2877,7 +2760,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", "Patient/patient-0", @@ -2906,8 +2788,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND (index_value = ? OR (index_value = ? OR index_value = ?)) ) @@ -2917,7 +2798,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", "Patient/patient-0", @@ -2948,8 +2828,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) ) @@ -2959,7 +2838,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", "Patient/patient-0", @@ -2987,8 +2865,7 @@ class SearchTest { """ SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a - WHERE a.resourceType = ? - AND a.resourceUuid IN ( + WHERE a.resourceUuid IN ( SELECT resourceUuid FROM ReferenceIndexEntity WHERE resourceType = ? AND index_name = ? AND (((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?)) OR ((index_value = ? OR index_value = ?) OR (index_value = ? OR index_value = ?))) ) @@ -2998,7 +2875,6 @@ class SearchTest { assertThat(query.args) .containsExactly( - "CarePlan", "CarePlan", "subject", *patientIdReferenceList.toTypedArray(), From 69b8ebf156c2282be64af8905d209260046fd33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:46:02 +0300 Subject: [PATCH 2/7] Check if previous/next page is hidden to show previous/next buttons (#2569) * Only select QuestionnaireItem with 'page' extension as page * Fix hasNextPage and hasPreviousPage to check if page is hidden * Resolve NIT, renaming test linkId to page4-noExtension-hidden --- .../datacapture/QuestionnaireViewModel.kt | 4 +- .../datacapture/QuestionnaireViewModelTest.kt | 178 +++++++++++++++++- 2 files changed, 179 insertions(+), 3 deletions(-) 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 056cd47aad..ecc019b0a8 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 @@ -1183,7 +1183,7 @@ internal data class QuestionnairePage( ) internal val QuestionnairePagination.hasPreviousPage: Boolean - get() = pages.any { it.index < currentPageIndex && it.enabled } + get() = pages.any { it.index < currentPageIndex && it.enabled && !it.hidden } internal val QuestionnairePagination.hasNextPage: Boolean - get() = pages.any { it.index > currentPageIndex && it.enabled } + get() = pages.any { it.index > currentPageIndex && it.enabled && !it.hidden } 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 51e00eaa1d..659429a95a 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 @@ -1533,6 +1533,102 @@ class QuestionnaireViewModelTest { // Pagination // // // // ==================================================================== // + @Test + fun `should include all top level items as pages when any item has page extension`() = runTest { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + QuestionnaireItemComponent().apply { + linkId = "page1-noExtension" + type = Questionnaire.QuestionnaireItemType.GROUP + addItem( + QuestionnaireItemComponent().apply { + linkId = "page1-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 1" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page2" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension(paginationExtension) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page2-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 2" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page3-noExtension" + type = Questionnaire.QuestionnaireItemType.GROUP + addItem( + QuestionnaireItemComponent().apply { + linkId = "page3-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 3" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page4-noExtension-hidden" + addExtension(hiddenExtension) + type = Questionnaire.QuestionnaireItemType.GROUP + addItem( + QuestionnaireItemComponent().apply { + linkId = "page4-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 4" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page5" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension(paginationExtension) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page5-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 5" + }, + ) + }, + ) + } + + val viewModel = createQuestionnaireViewModel(questionnaire) + viewModel.runViewModelBlocking { + assertThat( + (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination, + ) + .isEqualTo( + QuestionnairePagination( + isPaginated = true, + pages = + listOf( + QuestionnairePage(0, enabled = true, hidden = false), + QuestionnairePage(1, enabled = true, hidden = false), + QuestionnairePage(2, enabled = true, hidden = false), + QuestionnairePage(3, enabled = true, hidden = true), + QuestionnairePage(4, enabled = true, hidden = false), + ), + currentPageIndex = 0, + ), + ) + } + } @Test fun `should show current page`() = runTest { @@ -1830,8 +1926,11 @@ class QuestionnaireViewModelTest { } val viewModel = createQuestionnaireViewModel(questionnaire) viewModel.runViewModelBlocking { + val questionnaireStatePagination = + (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination + assertThat( - (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination, + questionnaireStatePagination, ) .isEqualTo( QuestionnairePagination( @@ -1845,6 +1944,83 @@ class QuestionnaireViewModelTest { currentPageIndex = 1, ), ) + + assertThat(questionnaireStatePagination.hasPreviousPage).isFalse() + } + } + + @Test + fun `should skip last page if it is hidden`() = runTest { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + QuestionnaireItemComponent().apply { + linkId = "page1" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension(paginationExtension) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page1-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 1" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page2" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension(paginationExtension) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page2-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 2" + }, + ) + }, + ) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page3" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension(paginationExtension) + addExtension(hiddenExtension) + addItem( + QuestionnaireItemComponent().apply { + linkId = "page3-1" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + text = "Question on page 3" + }, + ) + }, + ) + } + val viewModel = createQuestionnaireViewModel(questionnaire) + viewModel.runViewModelBlocking { + viewModel.goToNextPage() + val questionnaireStatePagination = + (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination + + assertThat( + questionnaireStatePagination, + ) + .isEqualTo( + QuestionnairePagination( + isPaginated = true, + pages = + listOf( + QuestionnairePage(0, enabled = true, hidden = false), + QuestionnairePage(1, enabled = true, hidden = false), + QuestionnairePage(2, enabled = true, hidden = true), + ), + currentPageIndex = 1, + ), + ) + + assertThat(questionnaireStatePagination.hasNextPage).isFalse() } } From a4d0929ba5ab9de38b2e317586600bb3c2c05e07 Mon Sep 17 00:00:00 2001 From: aditya-07 Date: Tue, 31 Dec 2024 23:14:14 +0530 Subject: [PATCH 3/7] Workflow api upgrade (#2744) * Updated activity flow api and used that in demo app * spotless apply * Updated review comments * Review comments: Added note * review comment: fixed --- .../fhir/workflow/activity/ActivityFlow.kt | 155 ++++++++++++++++ .../fhir/workflow/activity/phase/Phase.kt | 12 +- .../activity/phase/event/PerformPhase.kt | 4 +- .../resource/event/CPGEventResource.kt | 23 ++- .../resource/request/CPGRequestResource.kt | 11 +- .../workflow/activity/ActivityFlowTest.kt | 41 +++++ .../workflow/demo/ui/main/ActivityHandler.kt | 2 +- .../workflow/demo/ui/main/MainViewModel.kt | 167 ++++++------------ 8 files changed, 286 insertions(+), 129 deletions(-) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/activity/ActivityFlow.kt b/workflow/src/main/java/com/google/android/fhir/workflow/activity/ActivityFlow.kt index ede47ac171..aa4c264b49 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/activity/ActivityFlow.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/activity/ActivityFlow.kt @@ -17,23 +17,36 @@ package com.google.android.fhir.workflow.activity import androidx.annotation.WorkerThread +import ca.uhn.fhir.model.api.IQueryParameterType +import ca.uhn.fhir.rest.param.ReferenceParam import com.google.android.fhir.workflow.activity.phase.Phase import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.ORDER import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PERFORM import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PLAN import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PROPOSAL +import com.google.android.fhir.workflow.activity.phase.ReadOnlyRequestPhase import com.google.android.fhir.workflow.activity.phase.event.PerformPhase +import com.google.android.fhir.workflow.activity.phase.event.PerformPhase.Companion.`class` +import com.google.android.fhir.workflow.activity.phase.idType import com.google.android.fhir.workflow.activity.phase.request.OrderPhase import com.google.android.fhir.workflow.activity.phase.request.PlanPhase import com.google.android.fhir.workflow.activity.phase.request.ProposalPhase import com.google.android.fhir.workflow.activity.resource.event.CPGCommunicationEvent import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource import com.google.android.fhir.workflow.activity.resource.event.CPGOrderMedicationEvent +import com.google.android.fhir.workflow.activity.resource.event.EventStatus import com.google.android.fhir.workflow.activity.resource.request.CPGCommunicationRequest import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationRequest import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource import com.google.android.fhir.workflow.activity.resource.request.Intent +import com.google.android.fhir.workflow.activity.resource.request.Status +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Communication +import org.hl7.fhir.r4.model.CommunicationRequest +import org.hl7.fhir.r4.model.MedicationDispense +import org.hl7.fhir.r4.model.MedicationRequest +import org.hl7.fhir.r4.model.Reference import org.opencds.cqf.fhir.api.Repository /** @@ -181,6 +194,37 @@ private constructor( return currentPhase } + /** Returns a read only list of all the previous phases of the flow. */ + fun getPreviousPhases(): List> { + val phases = mutableListOf>() + var current: Phase? = currentPhase + + while (current != null) { + val basedOn: Reference? = + if (current is Phase.RequestPhase<*>) { + (current).getRequestResource().getBasedOn() + } else if (current is Phase.EventPhase<*>) { + (current).getEventResource().getBasedOn() + } else { + null + } + + val basedOnRequest = + basedOn?.let { + repository.read(it.`class`, it.idType)?.let { CPGRequestResource.of(it) as R } + } + current = + when (basedOnRequest?.getIntent()) { + Intent.PROPOSAL -> ProposalPhase(repository, basedOnRequest) + Intent.PLAN -> PlanPhase(repository, basedOnRequest) + Intent.ORDER -> OrderPhase(repository, basedOnRequest) + else -> null + } + current?.let { phases.add(it as ReadOnlyRequestPhase) } + } + return phases + } + /** * Prepares a plan resource based on the state of the [currentPhase] and returns it to the caller * without persisting any changes into [repository]. @@ -303,5 +347,116 @@ private constructor( resource: CPGOrderMedicationEvent<*>, ): ActivityFlow> = ActivityFlow(repository, null, resource) + + /** Returns a list of active flows associated with the [patientId]. */ + fun of( + repository: Repository, + patientId: String, + ): List, CPGEventResource<*>>> { + /** + * NOTE: After adding a new + * [activity](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html), add + * appropriate resource classes to eventTypes & requestTypes for the api to be able to search + * for flows in database. + */ + val eventTypes = + listOf( + MedicationDispense::class.java, + Communication::class.java, + ) + + val events = + eventTypes + .flatMap { + repository + .search( + Bundle::class.java, + it, + mutableMapOf>( + "subject" to mutableListOf(ReferenceParam("Patient/$patientId")), + ), + null, + ) + .entry + .map { it.resource } + } + .map { CPGEventResource.of(it) } + + val requestTypes = + listOf( + MedicationRequest::class.java, + CommunicationRequest::class.java, + ) + + // This is used to fetch the `basedOn` resource for a request/event to form RequestChain + val idToRequestMap: MutableMap> = + requestTypes + .flatMap { + repository + .search( + Bundle::class.java, + it, + mutableMapOf>( + "subject" to mutableListOf(ReferenceParam("Patient/$patientId")), + ), + null, + ) + .entry + .map { it.resource } + } + .map { CPGRequestResource.of(it) } + .associateByTo(LinkedHashMap()) { "${it.resourceType}/${it.logicalId}" } + + fun addBasedOn( + request: RequestChain, + ): RequestChain? { + val basedOn = request.request?.getBasedOn() ?: request.event?.getBasedOn() + // look up the cache for the parent resource and add to the chain + return basedOn?.let { reference -> + idToRequestMap[reference.reference]?.let { requestResource -> + idToRequestMap.remove(reference.reference) + RequestChain(request = requestResource).apply { this.basedOn = addBasedOn(this) } + } + } + } + + val requestChain = + events.map { RequestChain(event = it).apply { this.basedOn = addBasedOn(this) } } + + idToRequestMap.values + .filter { + it.getIntent() == Intent.ORDER || + it.getIntent() == Intent.PLAN || + it.getIntent() == Intent.PROPOSAL + } + .sortedByDescending { it.getIntent().code } + .mapNotNull { + if (idToRequestMap.containsKey("${it.resourceType}/${it.logicalId}")) { + RequestChain(request = it).apply { this.basedOn = addBasedOn(this) } + } else { + null + } + } + return requestChain + .filter { + if (it.event != null) { + it.event.getStatus() != EventStatus.COMPLETED + } else if (it.request != null) { + it.request.getStatus() != Status.COMPLETED + } else { + false + } + } + .map { ActivityFlow(repository, it.request, it.event) } + } } } + +/** + * Represents the chain of event/requests of an activity flow. A [RequestChain] would either have a + * [request] or an [event]. + */ +private data class RequestChain( + val request: CPGRequestResource<*>? = null, + val event: CPGEventResource<*>? = null, + var basedOn: RequestChain? = null, +) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/Phase.kt b/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/Phase.kt index a11ce85e23..56910b6b1c 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/Phase.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/Phase.kt @@ -17,6 +17,7 @@ package com.google.android.fhir.workflow.activity.phase import androidx.annotation.WorkerThread +import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource import org.hl7.fhir.r4.model.IdType @@ -35,8 +36,7 @@ sealed interface Phase { fun getPhaseName(): PhaseName /** Activity Phases for a CPG Request. */ - interface RequestPhase> : Phase { - fun getRequestResource(): R + interface RequestPhase> : Phase, ReadOnlyRequestPhase { @WorkerThread fun update(r: R): Result @@ -77,3 +77,11 @@ internal fun checkEquals(a: Reference, b: Reference) = a.reference == b.referenc /** Returns an [IdType] of a [Reference]. This is required for [Repository.read] api. */ internal val Reference.idType get() = IdType(reference) + +/** Provides a read-only view of a request phase. */ +interface ReadOnlyRequestPhase> { + /** Returns the [Phase.PhaseName] of this phase. */ + fun getPhaseName(): PhaseName + + fun getRequestResource(): R +} diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/event/PerformPhase.kt b/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/event/PerformPhase.kt index 168c95bdca..60f0ce6cd9 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/event/PerformPhase.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/event/PerformPhase.kt @@ -139,7 +139,7 @@ class PerformPhase>( * Returns the [Resource] class for the resource. e.g. If the Reference is `Patient/1234`, then * this would return the `Class` for `org.hl7.fhir.r4.model.Patient`. */ - private val Reference.`class` + internal val Reference.`class` get() = getResourceClass(reference.split("/")[0]) /** @@ -165,7 +165,7 @@ class PerformPhase>( "${inputPhase.getPhaseName().name} request is still in ${inputRequest.getStatusCode()} status." } - val eventRequest = CPGEventResource.of(inputRequest, eventClass) + val eventRequest = CPGEventResource.from(inputRequest, eventClass) eventRequest.setStatus(EventStatus.PREPARATION) eventRequest.setBasedOn(inputRequest.asReference()) eventRequest as E diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/event/CPGEventResource.kt b/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/event/CPGEventResource.kt index 699545c416..3d6cc6018c 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/event/CPGEventResource.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/event/CPGEventResource.kt @@ -23,6 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationR import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of import org.hl7.fhir.r4.model.Communication +import org.hl7.fhir.r4.model.MedicationDispense import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -41,7 +42,7 @@ import org.hl7.fhir.r4.model.ResourceType * [CPGEventResource]s. */ sealed class CPGEventResource( - internal open val resource: R, + open val resource: R, internal val mapper: EventStatusCodeMapper, ) where R : Resource { @@ -65,12 +66,22 @@ sealed class CPGEventResource( companion object { - fun of(request: CPGRequestResource<*>, eventClass: Class<*>): CPGEventResource<*> { - return when (request) { - is CPGCommunicationRequest -> CPGCommunicationEvent.from(request) - is CPGMedicationRequest -> CPGOrderMedicationEvent.from(request, eventClass) + internal fun from(from: CPGRequestResource<*>, to: Class<*>): CPGEventResource<*> { + return when (from) { + is CPGCommunicationRequest -> CPGCommunicationEvent.from(from) + is CPGMedicationRequest -> CPGOrderMedicationEvent.from(from, to) else -> { - throw IllegalArgumentException("Unknown CPG Request type ${request::class}.") + throw IllegalArgumentException("Unknown CPG Request type ${from::class}.") + } + } + } + + fun of(event: Resource): CPGEventResource<*> { + return when (event) { + is Communication -> CPGCommunicationEvent(event) + is MedicationDispense -> CPGMedicationDispenseEvent(event) + else -> { + throw IllegalArgumentException("Unknown CPG event type ${event::class}.") } } } diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/request/CPGRequestResource.kt b/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/request/CPGRequestResource.kt index 34a959fa2c..33cf02126a 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/request/CPGRequestResource.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/request/CPGRequestResource.kt @@ -19,7 +19,6 @@ package com.google.android.fhir.workflow.activity.resource.request import com.google.android.fhir.logicalId import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of import com.google.android.fhir.workflow.activity.resource.request.Intent.ORDER -import com.google.android.fhir.workflow.activity.resource.request.Intent.OTHER import com.google.android.fhir.workflow.activity.resource.request.Intent.PLAN import com.google.android.fhir.workflow.activity.resource.request.Intent.PROPOSAL import org.hl7.fhir.r4.model.CommunicationRequest @@ -28,8 +27,6 @@ import org.hl7.fhir.r4.model.MedicationRequest import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType -import org.hl7.fhir.r4.model.ServiceRequest -import org.hl7.fhir.r4.model.Task /** * This abstracts the @@ -52,7 +49,7 @@ import org.hl7.fhir.r4.model.Task * create the appropriate [CPGRequestResource]. */ sealed class CPGRequestResource( - internal open val resource: R, + open val resource: R, internal val mapper: StatusCodeMapper, ) where R : Resource { @@ -64,7 +61,7 @@ sealed class CPGRequestResource( internal abstract fun setIntent(intent: Intent) - internal abstract fun getIntent(): Intent + abstract fun getIntent(): Intent abstract fun setStatus(status: Status, reason: String? = null) @@ -125,9 +122,7 @@ sealed class CPGRequestResource( */ fun of(resource: R): CPGRequestResource { return when (resource) { - is Task -> of(resource) is MedicationRequest -> of(resource) - is ServiceRequest -> of(resource) is CommunicationRequest -> of(resource) else -> { throw IllegalArgumentException("Unknown CPG Request type ${resource::class}.") @@ -145,7 +140,7 @@ sealed class CPGRequestResource( * See [codesystem-request-intent](https://www.hl7.org/FHIR/codesystem-request-intent.html) for the * list of intents. */ -internal sealed class Intent(val code: String?) { +sealed class Intent(val code: String?) { data object PROPOSAL : Intent("proposal") data object PLAN : Intent("plan") diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/activity/ActivityFlowTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/activity/ActivityFlowTest.kt index 6356b26b78..2058ea6e34 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/activity/ActivityFlowTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/activity/ActivityFlowTest.kt @@ -642,4 +642,45 @@ class ActivityFlowTest { // check that the flow is still in old phase (proposal). assertThat(flow.getCurrentPhase().getPhaseName()).isEqualTo(Phase.PhaseName.PROPOSAL) } + + @Test + fun `getPreviousPhases should return a list of all previous phases`(): Unit = + runBlockingOnWorkerThread { + val cpgCommunicationRequest = + CPGRequestResource.of( + CommunicationRequest().apply { + id = "com-req-01" + status = CommunicationRequest.CommunicationRequestStatus.ACTIVE + subject = Reference("Patient/pat-01") + meta.addProfile( + "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationrequest", + ) + + addPayload().apply { content = StringType("Proposal") } + }, + ) + .apply { setIntent(Intent.PROPOSAL) } + val repository = FhirEngineRepository(FhirContext.forR4Cached(), fhirEngine) + repository.create(cpgCommunicationRequest.resource) + + val flow = ActivityFlow.of(repository, cpgCommunicationRequest) + + flow.initiatePlan( + flow.preparePlan().getOrThrow().apply { setStatus(Status.ACTIVE) }, + ) + + flow.initiateOrder( + flow.prepareOrder().getOrThrow().apply { setStatus(Status.ACTIVE) }, + ) + + flow.initiatePerform( + flow.preparePerform(CPGCommunicationEvent::class.java).getOrThrow().apply { + setStatus(EventStatus.INPROGRESS) + }, + ) + + val result = flow.getPreviousPhases() + assertThat(result.map { it.getPhaseName() }) + .containsExactly(Phase.PhaseName.ORDER, Phase.PhaseName.PLAN, Phase.PhaseName.PROPOSAL) + } } diff --git a/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/ActivityHandler.kt b/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/ActivityHandler.kt index bdc668ad59..cbb6d96a0e 100644 --- a/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/ActivityHandler.kt +++ b/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/ActivityHandler.kt @@ -23,7 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGRequestReso import com.google.android.fhir.workflow.activity.resource.request.Status class ActivityHandler( - private val activityFlow: ActivityFlow, CPGEventResource<*>>, + val activityFlow: ActivityFlow, CPGEventResource<*>>, ) { suspend fun prepareAndInitiatePlan(): Result { diff --git a/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/MainViewModel.kt b/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/MainViewModel.kt index 9116182bc4..4e076721f8 100644 --- a/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/MainViewModel.kt +++ b/workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/MainViewModel.kt @@ -25,18 +25,18 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.FhirEngineConfiguration import com.google.android.fhir.FhirEngineProvider import com.google.android.fhir.knowledge.KnowledgeManager -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import com.google.android.fhir.workflow.activity.ActivityFlow +import com.google.android.fhir.workflow.activity.phase.Phase import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource import com.google.android.fhir.workflow.activity.resource.event.CPGMedicationDispenseEvent -import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationRequest import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource import com.google.android.fhir.workflow.repositories.FhirEngineRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.MedicationDispense @@ -89,42 +89,22 @@ class MainViewModel(private val application: Application) : AndroidViewModel(app val adapterData = combine(activityOptionFlow, enabledPhaseFlow) { configuration, phase -> - Log.d("TAG", "phaseFlow configuration: $configuration phase: $phase") - val nextPhase = loadChainAndReturnNextPhase() - enabledPhaseFlow.value = nextPhase - - // Initialize the activity flow - if (activityHandler == null) { - val resource = - if (order != null) { - order - } else if (plan != null) { - plan - } else if (proposal != null) { - proposal - } else { - null - } + Log.d("MVModel", "phaseFlow configuration: ${configuration.id} phase: $phase") + val nextPhase = loadChainAndReturnNextPhase() - resource?.let { - activityHandler = - perform?.let { - ActivityHandler( - ActivityFlow.of(repository, CPGMedicationDispenseEvent(it as MedicationDispense)) - as ActivityFlow, CPGEventResource<*>>, - ) - } - ?: ActivityHandler( - ActivityFlow.of(repository, CPGRequestResource.of(it) as CPGMedicationRequest) - as ActivityFlow, CPGEventResource<*>>, - ) + // Initialize the activity flow + if (activityHandler == null) { + ActivityFlow.of(repository, "active_apple_guy").firstOrNull()?.let { + activityHandler = ActivityHandler(it) + } } + enabledPhaseFlow.value = nextPhase + generateData(nextPhase, ::handleOnClick) } - generateData(nextPhase, ::handleOnClick) - } + .flowOn(Dispatchers.IO) private fun handleOnClick(phase: FlowPhase) { - Log.d("TAG", "handleOnClick: $phase") + Log.d("MVModel", "handleOnClick: $phase") viewModelScope.launch(Dispatchers.IO) { when (phase) { FlowPhase.PROPOSAL -> { @@ -206,36 +186,41 @@ class MainViewModel(private val application: Application) : AndroidViewModel(app } private suspend fun loadChainAndReturnNextPhase(): FlowPhase { - // TODO : Add logic to load resources as per the selected configuration. - val request = - fhirEngine.search { - filter(MedicationRequest.SUBJECT, { value = "Patient/active_apple_guy" }) - } - - val request2 = - fhirEngine.search { - filter(MedicationRequest.SUBJECT, { value = "Patient/active_apple_guy" }) - } - - // take the last resource and move backwards - proposal = null plan = null order = null perform = null - request.forEach { - if (it.resource.intent == MedicationRequest.MedicationRequestIntent.PROPOSAL) { - proposal = it.resource - } else if (it.resource.intent == MedicationRequest.MedicationRequestIntent.PLAN) { - plan = it.resource - } else if (it.resource.intent == MedicationRequest.MedicationRequestIntent.ORDER) { - order = it.resource + + fun setPhase(curPhase: Phase.PhaseName, resource: Resource) { + if (curPhase == Phase.PhaseName.PERFORM) { + perform = resource + } else if (curPhase == Phase.PhaseName.ORDER) { + order = resource + } else if (curPhase == Phase.PhaseName.PLAN) { + plan = resource + } else if (curPhase == Phase.PhaseName.PROPOSAL) { + proposal = resource + } + } + + fun setPhase(curPhase: Phase) { + if (curPhase is Phase.EventPhase<*>) { + setPhase(curPhase.getPhaseName(), curPhase.getEventResource().resource) } else { - // perform = it.resource + setPhase( + curPhase.getPhaseName(), + (curPhase as Phase.RequestPhase<*>).getRequestResource().resource, + ) } } - perform = request2.firstOrNull()?.resource + activityHandler?.let { + val curPhase = it.activityFlow.getCurrentPhase() + setPhase(curPhase) + it.activityFlow.getPreviousPhases().forEach { + setPhase(it.getPhaseName(), it.getRequestResource().resource) + } + } return if (!proposalHandler.checkInstalledDependencies(activityOptionFlow.value)) { FlowPhase.INITIALIZE @@ -287,79 +272,41 @@ class MainViewModel(private val application: Application) : AndroidViewModel(app return list } - // TODO: CPGRequestResource : make getIntent public and then uncomment this. - // private fun getPhaseDetails(requestResource: Resource?): String { - // return if (requestResource == null) { - // "" - // } else if (requestResource is MedicationRequest) { - // val cpgRequestResource = CPGRequestResource.of(requestResource) - // - // val dosage = - // FhirContext.forR4Cached() - // .newJsonParser() - // .encodeToString(requestResource.dosageInstruction.first()) - // - // """ - // ID : ${cpgRequestResource.resourceType}/${cpgRequestResource.logicalId} - // Intent : ${cpgRequestResource.getIntent().code} - // Status : ${cpgRequestResource.getStatusCode()} - // BasedOn: ${cpgRequestResource.getBasedOn()?.reference} - // - // Additional Info: $dosage - // """ - // .trimIndent() - // } else if (requestResource is MedicationDispense) { - // val cpgRequestResource = CPGMedicationDispenseEvent(requestResource) - // - // val dosage = - // FhirContext.forR4Cached() - // .newJsonParser() - // .encodeToString(requestResource.dosageInstruction.first()) - // - // """ - // ID : ${cpgRequestResource.resourceType}/${cpgRequestResource.logicalId} - // Status : ${cpgRequestResource.getStatusCode()} - // BasedOn: ${cpgRequestResource.getBasedOn()?.reference} - // - // Additional Info: $dosage - // """ - // .trimIndent() - // } else { - // "" - // } - // } - private fun getPhaseDetails(requestResource: Resource?): String { return if (requestResource == null) { "" } else if (requestResource is MedicationRequest) { + val cpgRequestResource = CPGRequestResource.of(requestResource) + val dosage = FhirContext.forR4Cached() .newJsonParser() .encodeToString(requestResource.dosageInstruction.first()) """ - ID : ${requestResource.resourceType}/${requestResource.logicalId} - Intent : ${requestResource.intent.display} - Status : ${requestResource.status.display} - BasedOn: ${requestResource.getBasedOn().firstOrNull()?.reference} - - Additional Info: $dosage - """ + ID : ${cpgRequestResource.resourceType}/${cpgRequestResource.logicalId} + Intent : ${cpgRequestResource.getIntent().code} + Status : ${cpgRequestResource.getStatusCode()} + BasedOn: ${cpgRequestResource.getBasedOn()?.reference} + + Additional Info: $dosage + """ .trimIndent() } else if (requestResource is MedicationDispense) { + val cpgRequestResource = CPGMedicationDispenseEvent(requestResource) + val dosage = FhirContext.forR4Cached() .newJsonParser() .encodeToString(requestResource.dosageInstruction.first()) """ - ID : ${requestResource.resourceType}/${requestResource.logicalId} - Status : ${requestResource.status.display} - BasedOn: ${requestResource.authorizingPrescription.firstOrNull()?.reference} - - Additional Info: $dosage - """ + ID : ${cpgRequestResource.resourceType}/${cpgRequestResource.logicalId} + Status : ${cpgRequestResource.getStatusCode()} + BasedOn: ${cpgRequestResource.getBasedOn()?.reference} + + Additional Info: $dosage + """ .trimIndent() } else { "" From 7bc8b075cb275236db03b8abb3a913b14720b987 Mon Sep 17 00:00:00 2001 From: aditya-07 Date: Wed, 1 Jan 2025 01:38:07 +0530 Subject: [PATCH 4/7] Add a new Time selector widget (#2760) * Add a new Time selector widget * Added tests * fixed failing test --- .../main/assets/component_time_picker.json | 36 +++++ ...component_time_picker_with_validation.json | 37 +++++ .../fhir/catalog/ComponentListViewModel.kt | 7 + catalog/src/main/res/values/strings.xml | 1 + .../datacapture/QuestionnaireEditAdapter.kt | 3 + .../QuestionnaireViewHolderType.kt | 3 +- .../factories/TimePickerViewHolderFactory.kt | 151 ++++++++++++++++++ .../src/main/res/layout/time_picker_view.xml | 57 +++++++ .../QuestionnaireViewHolderTypeTest.kt | 4 +- .../views/TimePickerViewHolderFactoryTest.kt | 132 +++++++++++++++ 10 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 catalog/src/main/assets/component_time_picker.json create mode 100644 catalog/src/main/assets/component_time_picker_with_validation.json create mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt create mode 100644 datacapture/src/main/res/layout/time_picker_view.xml create mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt diff --git a/catalog/src/main/assets/component_time_picker.json b/catalog/src/main/assets/component_time_picker.json new file mode 100644 index 0000000000..ac904d99c8 --- /dev/null +++ b/catalog/src/main/assets/component_time_picker.json @@ -0,0 +1,36 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "text": "Enter a time", + "type": "time", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/entryFormat", + "valueString": "hh-mm" + } + ], + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-display-category", + "code": "instructions" + } + ] + } + } + ], + "linkId": "1-most-recent", + "text": "Use keyboard entry or time picker", + "type": "display" + } + ] + } + ] +} \ No newline at end of file diff --git a/catalog/src/main/assets/component_time_picker_with_validation.json b/catalog/src/main/assets/component_time_picker_with_validation.json new file mode 100644 index 0000000000..198a8f5bea --- /dev/null +++ b/catalog/src/main/assets/component_time_picker_with_validation.json @@ -0,0 +1,37 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "text": "Enter a time", + "type": "time", + "required": true, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/entryFormat", + "valueString": "hh-mm" + } + ], + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-display-category", + "code": "instructions" + } + ] + } + } + ], + "linkId": "1-most-recent", + "text": "Use keyboard entry or time picker", + "type": "display" + } + ] + } + ] +} \ No newline at end of file diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index d9e4637ada..00f541e5c6 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -102,6 +102,12 @@ class ComponentListViewModel(application: Application, private val state: SavedS "component_date_picker.json", "component_date_picker_with_validation.json", ), + TIME_PICKER( + R.drawable.ic_timepicker, + R.string.component_name_time_picker, + "component_time_picker.json", + "component_time_picker_with_validation.json", + ), DATE_TIME_PICKER( R.drawable.ic_timepicker, R.string.component_name_date_time_picker, @@ -171,6 +177,7 @@ class ComponentListViewModel(application: Application, private val state: SavedS ViewItem.ComponentItem(Component.TEXT_FIELD), ViewItem.ComponentItem(Component.AUTO_COMPLETE), ViewItem.ComponentItem(Component.DATE_PICKER), + ViewItem.ComponentItem(Component.TIME_PICKER), ViewItem.ComponentItem(Component.DATE_TIME_PICKER), ViewItem.ComponentItem(Component.SLIDER), ViewItem.ComponentItem(Component.QUANTITY), diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index ae47067784..69b5f26fa8 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Text field Auto Complete Date picker + Time picker DateTime picker Slider Quantity diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt index 90f43a53c6..6fc427eae2 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt @@ -47,6 +47,7 @@ import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemView import com.google.android.fhir.datacapture.views.factories.RadioGroupViewHolderFactory import com.google.android.fhir.datacapture.views.factories.RepeatedGroupHeaderItemViewHolder import com.google.android.fhir.datacapture.views.factories.SliderViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType internal class QuestionnaireEditAdapter( @@ -103,6 +104,7 @@ internal class QuestionnaireEditAdapter( QuestionnaireViewHolderType.GROUP -> GroupViewHolderFactory QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER -> BooleanChoiceViewHolderFactory QuestionnaireViewHolderType.DATE_PICKER -> DatePickerViewHolderFactory + QuestionnaireViewHolderType.TIME_PICKER -> TimePickerViewHolderFactory QuestionnaireViewHolderType.DATE_TIME_PICKER -> DateTimePickerViewHolderFactory QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE -> EditTextSingleLineViewHolderFactory QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE -> EditTextMultiLineViewHolderFactory @@ -223,6 +225,7 @@ internal class QuestionnaireEditAdapter( QuestionnaireItemType.GROUP -> QuestionnaireViewHolderType.GROUP QuestionnaireItemType.BOOLEAN -> QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER QuestionnaireItemType.DATE -> QuestionnaireViewHolderType.DATE_PICKER + QuestionnaireItemType.TIME -> QuestionnaireViewHolderType.TIME_PICKER QuestionnaireItemType.DATETIME -> QuestionnaireViewHolderType.DATE_TIME_PICKER QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireViewItem) QuestionnaireItemType.TEXT -> QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt index 5a64806841..d9442a652a 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ enum class QuestionnaireViewHolderType(val value: Int) { SLIDER(15), PHONE_NUMBER(16), ATTACHMENT(17), + TIME_PICKER(18), ; companion object { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt new file mode 100644 index 0000000000..91062b1de8 --- /dev/null +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.views.factories + +import android.annotation.SuppressLint +import android.content.Context +import android.text.InputType +import android.text.format.DateFormat +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.google.android.fhir.datacapture.R +import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText +import com.google.android.fhir.datacapture.extensions.toLocalizedString +import com.google.android.fhir.datacapture.extensions.tryUnwrapContext +import com.google.android.fhir.datacapture.views.HeaderView +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_CLOCK +import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_KEYBOARD +import com.google.android.material.timepicker.TimeFormat +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.TimeType + +object TimePickerViewHolderFactory : QuestionnaireItemViewHolderFactory(R.layout.time_picker_view) { + + override fun getQuestionnaireItemViewHolderDelegate() = + object : QuestionnaireItemViewHolderDelegate { + private val TAG = "time-picker" + private lateinit var context: AppCompatActivity + private lateinit var header: HeaderView + private lateinit var timeInputLayout: TextInputLayout + private lateinit var timeInputEditText: TextInputEditText + override lateinit var questionnaireViewItem: QuestionnaireViewItem + + override fun init(itemView: View) { + context = itemView.context.tryUnwrapContext()!! + header = itemView.findViewById(R.id.header) + timeInputLayout = itemView.findViewById(R.id.text_input_layout) + timeInputEditText = itemView.findViewById(R.id.text_input_edit_text) + timeInputEditText.inputType = InputType.TYPE_NULL + timeInputEditText.hint = itemView.context.getString(R.string.time) + + timeInputLayout.setEndIconOnClickListener { + // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment + // and again in TextInputEditText during layout inflation. As a result, it is + // necessary to access the base context twice to retrieve the application object + // from the view's context. + val context = itemView.context.tryUnwrapContext()!! + buildMaterialTimePicker(context, INPUT_MODE_CLOCK) + } + timeInputEditText.setOnClickListener { + buildMaterialTimePicker(itemView.context, INPUT_MODE_KEYBOARD) + } + } + + @SuppressLint("NewApi") // java.time APIs can be used due to desugaring + override fun bind(questionnaireViewItem: QuestionnaireViewItem) { + clearPreviousState() + header.bind(questionnaireViewItem) + timeInputLayout.helperText = getRequiredOrOptionalText(questionnaireViewItem, context) + + val questionnaireItemViewItemDateTimeAnswer = + questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime + + // If there is no set answer in the QuestionnaireItemViewItem, make the time field empty. + timeInputEditText.setText( + questionnaireItemViewItemDateTimeAnswer?.toLocalizedString(timeInputEditText.context) + ?: "", + ) + } + + override fun setReadOnly(isReadOnly: Boolean) { + // The system outside this delegate should only be able to mark it read only. Otherwise, it + // will change the state set by this delegate in bindView(). + if (isReadOnly) { + timeInputEditText.isEnabled = false + timeInputLayout.isEnabled = false + } + } + + private fun buildMaterialTimePicker(context: Context, inputMode: Int) { + val selectedTime = + questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime ?: LocalTime.now() + val timeFormat = + if (DateFormat.is24HourFormat(context)) { + TimeFormat.CLOCK_24H + } else { + TimeFormat.CLOCK_12H + } + MaterialTimePicker.Builder() + .setTitleText(R.string.select_time) + .setHour(selectedTime.hour) + .setMinute(selectedTime.minute) + .setTimeFormat(timeFormat) + .setInputMode(inputMode) + .build() + .apply { + addOnPositiveButtonClickListener { + with(LocalTime.of(this.hour, this.minute, 0)) { + timeInputEditText.setText(this.toLocalizedString(context)) + setQuestionnaireItemViewItemAnswer(this) + timeInputEditText.clearFocus() + } + } + } + .show(context.tryUnwrapContext()!!.supportFragmentManager, TAG) + } + + /** Set the answer in the [QuestionnaireResponse]. */ + private fun setQuestionnaireItemViewItemAnswer(localDateTime: LocalTime) = + context.lifecycleScope.launch { + questionnaireViewItem.setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(TimeType(localDateTime.format(DateTimeFormatter.ISO_TIME))), + ) + } + + private fun clearPreviousState() { + timeInputEditText.isEnabled = true + timeInputLayout.isEnabled = true + } + } + + private val TimeType.localTime + get() = + LocalTime.of( + hour, + minute, + second.toInt(), + ) +} diff --git a/datacapture/src/main/res/layout/time_picker_view.xml b/datacapture/src/main/res/layout/time_picker_view.xml new file mode 100644 index 0000000000..2ad1cd5563 --- /dev/null +++ b/datacapture/src/main/res/layout/time_picker_view.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt index 7b971501fb..04f45d42a6 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.robolectric.annotation.Config class QuestionnaireViewHolderTypeTest { @Test fun size_shouldReturnNumberOfQuestionnaireViewHolderTypes() { - assertThat(QuestionnaireViewHolderType.values().size).isEqualTo(18) + assertThat(QuestionnaireViewHolderType.values().size).isEqualTo(19) } @Test diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt new file mode 100644 index 0000000000..90e4d5bc3f --- /dev/null +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.views + +import android.widget.FrameLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.google.android.fhir.datacapture.R +import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder +import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.TimeType +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowSettings + +@RunWith(RobolectricTestRunner::class) +class TimePickerViewHolderFactoryTest { + private val context = + Robolectric.buildActivity(AppCompatActivity::class.java).create().get().apply { + setTheme(com.google.android.material.R.style.Theme_Material3_DayNight) + } + private val parent = FrameLayout(context) + private val viewHolder = TimePickerViewHolderFactory.create(parent) + + private val QuestionnaireItemViewHolder.timeInputView: TextView + get() { + return itemView.findViewById(R.id.text_input_edit_text) + } + + @Test + fun shouldSetQuestionHeader() { + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + + assertThat(viewHolder.itemView.findViewById(R.id.question).text.toString()) + .isEqualTo("Question?") + } + + @Test + fun shouldSetEmptyTimeInput() { + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("") + } + + @Test + fun `should show AM time when set time format is 12 hrs`() { + ShadowSettings.set24HourTimeFormat(false) + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(TimeType("10:10")), + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("10:10 AM") + } + + @Test + fun `should show PM time when set time format is 12 hrs`() { + ShadowSettings.set24HourTimeFormat(false) + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(TimeType("22:10:10")), + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("10:10 PM") + } + + @Test + fun `should show time when set time format is 24 hrs`() { + ShadowSettings.set24HourTimeFormat(true) + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(TimeType("22:10")), + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("22:10") + } +} From 8d306be711011d06f4d1c9012a8171e478510e47 Mon Sep 17 00:00:00 2001 From: Rahul Malhotra <16497903+rahulmalhotra@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:06:22 +0530 Subject: [PATCH 5/7] Disable new lines for single line edit text (#2737) --- datacapture/src/main/res/layout/edit_text_single_line_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/datacapture/src/main/res/layout/edit_text_single_line_view.xml b/datacapture/src/main/res/layout/edit_text_single_line_view.xml index 7520fe616a..56bf4db06b 100644 --- a/datacapture/src/main/res/layout/edit_text_single_line_view.xml +++ b/datacapture/src/main/res/layout/edit_text_single_line_view.xml @@ -52,6 +52,7 @@ Date: Wed, 8 Jan 2025 15:17:38 +0530 Subject: [PATCH 6/7] Search use case in the demo app. (#2754) * Search use case. * Update ui edge cases. * spotless apply. * Address ui changes. * Code refactoring * address review comments. --------- Co-authored-by: Santosh Pingle --- .../android/fhir/demo/PatientListFragment.kt | 54 +++++++------ .../android/fhir/demo/PatientListViewModel.kt | 78 ++++++++++++++++++- .../main/res/layout/fragment_patient_list.xml | 49 ++++++++++-- demo/src/main/res/values/strings.xml | 4 +- 4 files changed, 153 insertions(+), 32 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index e61ab37648..31eeba82a8 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.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. @@ -28,6 +28,7 @@ import android.view.inputmethod.InputMethodManager import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.NavHostFragment @@ -53,6 +54,35 @@ class PatientListFragment : Fragment() { savedInstanceState: Bundle?, ): View { _binding = FragmentPatientListBinding.inflate(inflater, container, false) + + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + binding.givenNameEditText.apply { + addTextChangedListener( + onTextChanged = { text, _, _, _ -> + patientListViewModel.setPatientGivenName(text.toString()) + }, + ) + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + } + } + + binding.familyNameEditText.apply { + addTextChangedListener( + onTextChanged = { text, _, _, _ -> + patientListViewModel.setPatientFamilyName(text.toString()) + }, + ) + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + } + } + return binding.root } @@ -87,27 +117,6 @@ class PatientListFragment : Fragment() { binding.patientListContainer.patientCount.text = "$it Patient(s)" } - searchView = binding.search - searchView.setOnQueryTextListener( - object : SearchView.OnQueryTextListener { - override fun onQueryTextChange(newText: String): Boolean { - patientListViewModel.searchPatientsByName(newText) - return true - } - - override fun onQueryTextSubmit(query: String): Boolean { - patientListViewModel.searchPatientsByName(query) - return true - } - }, - ) - searchView.setOnQueryTextFocusChangeListener { view, focused -> - if (!focused) { - // hide soft keyboard - (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(view.windowToken, 0) - } - } requireActivity() .onBackPressedDispatcher .addCallback( @@ -123,7 +132,6 @@ class PatientListFragment : Fragment() { } }, ) - setHasOptionsMenu(true) } diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt index fbf7b43fae..8aa22a9b51 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.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. @@ -39,12 +39,11 @@ import org.hl7.fhir.r4.model.RiskAssessment */ class PatientListViewModel(application: Application, private val fhirEngine: FhirEngine) : AndroidViewModel(application) { - val liveSearchedPatients = MutableLiveData>() val patientCount = MutableLiveData() init { - updatePatientListAndPatientCount({ getSearchResults() }, { count() }) + updatePatientListAndPatientCount({ getSearchResults() }, { searchedPatientCount() }) } fun searchPatientsByName(nameQuery: String) { @@ -174,6 +173,79 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi throw IllegalArgumentException("Unknown ViewModel class") } } + + private var patientGivenName: String? = null + private var patientFamilyName: String? = null + + fun setPatientGivenName(givenName: String) { + patientGivenName = givenName + searchPatientsByParameter() + } + + fun setPatientFamilyName(familyName: String) { + patientFamilyName = familyName + searchPatientsByParameter() + } + + private fun searchPatientsByParameter() { + viewModelScope.launch { + liveSearchedPatients.value = searchPatients() + patientCount.value = searchedPatientCount() + } + } + + private suspend fun searchPatients(): List { + val patients = + fhirEngine + .search { + filter( + Patient.GIVEN, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientGivenName ?: "" + }, + ) + filter( + Patient.FAMILY, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientFamilyName ?: "" + }, + ) + sort(Patient.GIVEN, Order.ASCENDING) + count = 100 + from = 0 + } + .mapIndexed { index, fhirPatient -> fhirPatient.resource.toPatientItem(index + 1) } + .toMutableList() + + val risks = getRiskAssessments() + patients.forEach { patient -> + risks["Patient/${patient.resourceId}"]?.let { + patient.risk = it.prediction?.first()?.qualitativeRisk?.coding?.first()?.code + } + } + return patients + } + + private suspend fun searchedPatientCount(): Long { + return fhirEngine.count { + filter( + Patient.GIVEN, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientGivenName ?: "" + }, + ) + filter( + Patient.FAMILY, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientFamilyName ?: "" + }, + ) + } + } } internal fun Patient.toPatientItem(position: Int): PatientListViewModel.PatientItem { diff --git a/demo/src/main/res/layout/fragment_patient_list.xml b/demo/src/main/res/layout/fragment_patient_list.xml index 09cfd442fb..1fcf485a66 100644 --- a/demo/src/main/res/layout/fragment_patient_list.xml +++ b/demo/src/main/res/layout/fragment_patient_list.xml @@ -13,15 +13,54 @@ android:focusableInTouchMode="true" android:orientation="vertical" > - - + + + + + + + + + + + + + %1$s: %2$s\nEffective: %3$s - Find by Patient Name Are you sure you want to discard the answers? @@ -74,4 +73,7 @@ Last sync status: %1$s Last sync status: Not available Periodic sync + Search Patient by + Given name + Family name From 2a7e91f7e118569de896a033b1583cafc6cc1555 Mon Sep 17 00:00:00 2001 From: santosh-pingle <86107848+santosh-pingle@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:02:21 +0530 Subject: [PATCH 7/7] CRUD operation screen in the demo app. (#2746) * crud operation show case. * Tab layout, birthdate, and edge cases in tab switching. * code clean up. * code clean up * Address review comment. * Address ui changes. * clear ui state. * address review comments. * refactoring. --------- Co-authored-by: Santosh Pingle --- .../fhir/demo/CrudOperationFragment.kt | 338 ++++++++++++++++++ .../fhir/demo/CrudOperationViewModel.kt | 162 +++++++++ .../google/android/fhir/demo/HomeFragment.kt | 6 +- .../demo/helpers/PatientCreationHelper.kt | 53 ++- .../main/res/layout/fragment_crud_layout.xml | 233 ++++++++++++ demo/src/main/res/layout/fragment_home.xml | 42 ++- .../res/navigation/reference_nav_graph.xml | 10 + demo/src/main/res/values/strings.xml | 5 + demo/src/main/res/values/styles.xml | 12 + 9 files changed, 847 insertions(+), 14 deletions(-) create mode 100644 demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt create mode 100644 demo/src/main/java/com/google/android/fhir/demo/CrudOperationViewModel.kt create mode 100644 demo/src/main/res/layout/fragment_crud_layout.xml diff --git a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt new file mode 100644 index 0000000000..2694044bc8 --- /dev/null +++ b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2024-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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.demo + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckBox +import android.widget.EditText +import android.widget.RadioGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.NavHostFragment +import com.google.android.fhir.demo.helpers.PatientCreationHelper +import com.google.android.material.tabs.TabLayout +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.Enumerations + +class CrudOperationFragment : Fragment() { + private val crudOperationViewModel: CrudOperationViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return inflater.inflate(R.layout.fragment_crud_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setUpActionBar() + setHasOptionsMenu(true) + setupUiOnScreenLaunch() + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + crudOperationViewModel.patientUiState.collect { patientUiState -> + patientUiState?.let { + when (it.operationType) { + OperationType.CREATE -> { + Toast.makeText(requireContext(), "Patient is saved", Toast.LENGTH_SHORT).show() + } + OperationType.READ -> displayPatientDetails(it) + OperationType.UPDATE -> { + Toast.makeText(requireContext(), "Patient is updated", Toast.LENGTH_SHORT).show() + } + OperationType.DELETE -> { + // Reset the page as the patient has been deleted. + clearUiFieldValues() + configureFieldsForOperation(OperationType.DELETE) + Toast.makeText(requireContext(), "Patient is deleted", Toast.LENGTH_SHORT).show() + } + } + } + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + NavHostFragment.findNavController(this).navigateUp() + true + } + else -> false + } + } + + private fun setUpActionBar() { + (requireActivity() as AppCompatActivity).supportActionBar?.apply { + title = requireContext().getString(R.string.crud_operations) + setDisplayHomeAsUpEnabled(true) + } + } + + private fun setupUiOnScreenLaunch() { + setupTabLayoutChangeListener() + selectTab(TAB_CREATE) + setupUiForCrudOperation(OperationType.CREATE) + + requireView().findViewById