diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index eecbfb2b87..6616981e4a 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -176,9 +176,7 @@ dependencies { api(libs.timber) api(libs.converter.gson) api(libs.json.path) - api(libs.easy.rules.jexl) { - exclude(group = "commons-logging", module = "commons-logging") - } + api(libs.easy.rules.jexl) { exclude(group = "commons-logging", module = "commons-logging") } api(libs.data.capture) { isTransitive = true exclude(group = "ca.uhn.hapi.fhir") diff --git a/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt new file mode 100644 index 0000000000..8862f5e123 --- /dev/null +++ b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.util.extension + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.MediumTest +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.FhirEngineConfiguration +import com.google.android.fhir.FhirEngineProvider +import com.google.android.fhir.search.search +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@MediumTest +class FhirEngineExtensionKtTest { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var fhirEngine: FhirEngine + + @Before + fun setUp() { + FhirEngineProvider.init(FhirEngineConfiguration(testMode = true)) + fhirEngine = FhirEngineProvider.getInstance(context) + + val patients = (0..1000).map { Patient().apply { id = "test-patient-$it" } } + val questionnaires = (0..3).map { Questionnaire().apply { id = "test-questionnaire-$it" } } + runBlocking { fhirEngine.create(*patients.toTypedArray(), *questionnaires.toTypedArray()) } + } + + @After + fun tearDown() { + runBlocking { fhirEngine.clearDatabase() } + FhirEngineProvider.cleanup() + } + + @Test + fun test_search_time_searches_sequentially_and_short_running_query_waits() { + val fetchedResources = mutableListOf() + runBlocking { + launch { + val patients = fhirEngine.search {}.map { it.resource } + fetchedResources += patients + } + + launch { + val questionnaires = fhirEngine.search {}.map { it.resource } + fetchedResources += questionnaires + } + } + val indexOfResultOfShortQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Questionnaire } + val indexOfResultOfLongQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Patient } + Assert.assertTrue(indexOfResultOfShortQuery > indexOfResultOfLongQuery) + } + + @Test + fun test_batchedSearch_returns_short_running_query_and_long_running_does_not_block() { + val fetchedResources = mutableListOf() + runBlocking { + launch { + val patients = fhirEngine.batchedSearch {}.map { it.resource } + fetchedResources += patients + } + + launch { + val questionnaires = fhirEngine.search {} + fetchedResources + questionnaires + } + } + + val indexOfResultOfShortQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Questionnaire } + val indexOfResultOfLongQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Patient } + Assert.assertTrue(indexOfResultOfShortQuery < indexOfResultOfLongQuery) + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index a7106e4bf0..82c126faf4 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -29,6 +29,15 @@ import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStreamReader +import java.net.UnknownHostException +import java.util.Locale +import java.util.PropertyResourceBundle +import java.util.ResourceBundle +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.RequestBody.Companion.toRequestBody @@ -71,15 +80,6 @@ import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.helper.LocalizationHelper import retrofit2.HttpException import timber.log.Timber -import java.io.File -import java.io.FileNotFoundException -import java.io.InputStreamReader -import java.net.UnknownHostException -import java.util.Locale -import java.util.PropertyResourceBundle -import java.util.ResourceBundle -import javax.inject.Inject -import javax.inject.Singleton @Singleton class ConfigurationRegistry @@ -378,14 +378,18 @@ constructor( context.assets.list(String.format(BASE_CONFIG_PATH, appId))?.onEach { if (!supportedFileExtensions.contains(it.fileExtension)) { filesQueue.addLast(String.format(BASE_CONFIG_PATH, appId) + "/$it") - } else configFiles.add(String.format(BASE_CONFIG_PATH, appId) + "/$it") + } else { + configFiles.add(String.format(BASE_CONFIG_PATH, appId) + "/$it") + } } while (filesQueue.isNotEmpty()) { val currentPath = filesQueue.removeFirst() context.assets.list(currentPath)?.onEach { if (!supportedFileExtensions.contains(it.fileExtension)) { filesQueue.addLast("$currentPath/$it") - } else configFiles.add("$currentPath/$it") + } else { + configFiles.add("$currentPath/$it") + } } } return configFiles @@ -507,13 +511,14 @@ constructor( val resultBundle = if (isNonProxy()) { fhirResourceDataSourceGetBundle(resourceType, resourceIdList) - } else + } else { fhirResourceDataSource.post( requestBody = generateRequestBundle(resourceType, resourceIdList) .encodeResourceToString() .toRequestBody(NetworkModule.JSON_MEDIA_TYPE), ) + } processResultBundleEntries(resultBundle.entry) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 60da3c1f83..0c5bc2f57c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -169,5 +169,5 @@ enum class LinkIdType : Parcelable { READ_ONLY, BARCODE, LOCATION, - IDENTIFIER, + PREPOPULATION_EXCLUSION, } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index 9cb66a6e51..f545c4481e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -22,7 +22,6 @@ import kotlinx.serialization.Serializable import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.util.extension.interpolate - const val ICON_TYPE_LOCAL = "local" const val ICON_TYPE_REMOTE = "remote" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt index 732bd36d5d..7f6d5c7ddd 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt @@ -50,7 +50,7 @@ data class RegisterConfiguration( val filterDataByRelatedEntityLocation: Boolean = false, val topScreenSection: TopScreenSectionConfig? = null, val onSearchByQrSingleResultActions: List? = null, - val infiniteScroll: Boolean = true + val infiniteScroll: Boolean = false, ) : Configuration() { val onSearchByQrSingleResultValidActions = onSearchByQrSingleResultActions?.filter { it.trigger == ActionTrigger.ON_SEARCH_SINGLE_RESULT } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt index 8996c628f5..5e89b02a09 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt @@ -91,7 +91,9 @@ data class ButtonProperties( val interpolated = this.status.interpolate(computedValuesMap) return if (ServiceStatus.values().map { it.name }.contains(interpolated)) { ServiceStatus.valueOf(interpolated) - } else ServiceStatus.UPCOMING + } else { + ServiceStatus.UPCOMING + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt index ba9dd121b1..30c1059b21 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt @@ -56,8 +56,7 @@ fun List.retrieveListProperties(): List { ViewType.COLUMN -> viewPropertiesQueue.addAll((properties as ColumnProperties).children) ViewType.ROW -> viewPropertiesQueue.addAll((properties as RowProperties).children) ViewType.CARD -> viewPropertiesQueue.addAll((properties as CardViewProperties).content) - ViewType.LIST -> - viewPropertiesQueue.addAll((properties as ListProperties).registerCard.views) + ViewType.LIST -> viewPropertiesQueue.addAll((properties as ListProperties).registerCard.views) else -> {} } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt index 6574ed470f..7c3a47ab6f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt @@ -32,7 +32,9 @@ object ViewPropertiesSerializer : ): DeserializationStrategy { val jsonObject = element.jsonObject val viewType = jsonObject[VIEW_TYPE]?.jsonPrimitive?.content - require(viewType != null && ViewType.entries.toTypedArray().contains(ViewType.valueOf(viewType))) { + require( + viewType != null && ViewType.entries.toTypedArray().contains(ViewType.valueOf(viewType)), + ) { """Ensure that supported `viewType` property is included in your register view properties configuration. Supported types: ${ViewType.entries.toTypedArray()} Parsed JSON: $jsonObject diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt index 7751bd9487..8ce645822c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt @@ -37,12 +37,15 @@ import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.has import com.google.android.fhir.search.include import com.google.android.fhir.search.revInclude -import com.google.android.fhir.search.search import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.Option import com.jayway.jsonpath.PathNotFoundException import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.LinkedList +import java.util.UUID +import javax.inject.Inject +import kotlin.math.min import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -82,6 +85,7 @@ import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @@ -95,9 +99,6 @@ import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.engine.util.pmap import timber.log.Timber -import java.util.UUID -import javax.inject.Inject -import kotlin.math.min open class DefaultRepository @Inject @@ -136,7 +137,7 @@ constructor( ): List = withContext(dispatcherProvider.io()) { fhirEngine - .search { + .batchedSearch { filterByResourceTypeId(token, subjectType, subjectId) dataQueries.forEach { filterBy( @@ -149,7 +150,7 @@ constructor( } suspend inline fun search(search: Search) = - fhirEngine.search(search).map { it.resource } + fhirEngine.batchedSearch(search).map { it.resource } suspend inline fun count(search: Search) = fhirEngine.count(search) @@ -265,14 +266,14 @@ constructor( suspend fun loadManagingEntity(group: Group) = group.managingEntity?.let { reference -> fhirEngine - .search { + .batchedSearch { filter(RelatedPerson.RES_ID, { value = of(reference.extractId()) }) } .map { it.resource } .firstOrNull() ?.let { relatedPerson -> fhirEngine - .search { + .batchedSearch { filter( Patient.RES_ID, { value = of(relatedPerson.patient.extractId()) }, @@ -479,73 +480,80 @@ constructor( configComputedRuleValues: Map, ): RelatedResourceWrapper { val relatedResourceWrapper = RelatedResourceWrapper() - val relatedResourcesQueue = ArrayDeque, List?>>().apply { - addFirst(Pair(listOf(resource), relatedResourcesConfigs)) - } + val relatedResourcesQueue = + ArrayDeque, List?>>().apply { + addFirst(Pair(listOf(resource), relatedResourcesConfigs)) + } while (relatedResourcesQueue.isNotEmpty()) { val (currentResources, currentRelatedResourceConfigs) = relatedResourcesQueue.removeFirst() - val relatedResourceCountConfigs = currentRelatedResourceConfigs - ?.asSequence() - ?.filter { it.resultAsCount && !it.searchParameter.isNullOrEmpty() } - ?.toList() + val relatedResourceCountConfigs = + currentRelatedResourceConfigs + ?.asSequence() + ?.filter { it.resultAsCount && !it.searchParameter.isNullOrEmpty() } + ?.toList() relatedResourceCountConfigs?.forEach { resourceConfig -> - val search = Search(resourceConfig.resource).apply { - val filters = - currentResources.map { - val apply: ReferenceParamFilterCriterion.() -> Unit = { - value = it.logicalId.asReference(it.resourceType).reference + val search = + Search(resourceConfig.resource).apply { + val filters = + currentResources.map { + val apply: ReferenceParamFilterCriterion.() -> Unit = { + value = it.logicalId.asReference(it.resourceType).reference + } + apply } - apply - } - filter( - ReferenceClientParam(resourceConfig.searchParameter), - *filters.toTypedArray(), - ) - applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues - ) - } + filter( + ReferenceClientParam(resourceConfig.searchParameter), + *filters.toTypedArray(), + ) + applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) + } val key = resourceConfig.id ?: resourceConfig.resource.name if (resourceConfig.countResultConfig?.sumCounts == true) { search.count( onSuccess = { - relatedResourceWrapper.relatedResourceCountMap.getOrPut(key) { mutableListOf()} - .apply { - add( - RelatedResourceCount( - count = it, - relatedResourceType = resourceConfig.resource, - parentResourceId = resource.logicalId - ) - ) - } + relatedResourceWrapper.relatedResourceCountMap + .getOrPut(key) { mutableListOf() } + .apply { add(RelatedResourceCount(count = it)) } }, onFailure = { Timber.e( it, - "Error retrieving total count for all related resources identified by $key" + "Error retrieving total count for all related resources identified by $key", ) - } + }, + ) + } else { + computeCountForEachRelatedResource( + resources = currentResources, + resourceConfig = resourceConfig, + relatedResourceWrapper = relatedResourceWrapper, + configComputedRuleValues = configComputedRuleValues, ) } } - val searchResults = searchIncludedResources( - relatedResourcesConfigs = currentRelatedResourceConfigs, - resources = currentResources, - configComputedRuleValues = configComputedRuleValues - ) + val searchResults = + searchIncludedResources( + relatedResourcesConfigs = currentRelatedResourceConfigs, + resources = currentResources, + configComputedRuleValues = configComputedRuleValues, + ) val fwdIncludedRelatedConfigsMap = - currentRelatedResourceConfigs?.revIncludeRelatedResourceConfigs(false) - ?.groupBy { it.searchParameter!! }?.mapValues { it.value.first() } + currentRelatedResourceConfigs + ?.revIncludeRelatedResourceConfigs(false) + ?.groupBy { it.searchParameter!! } + ?.mapValues { it.value.first() } val revIncludedRelatedConfigsMap = - currentRelatedResourceConfigs?.revIncludeRelatedResourceConfigs(true) + currentRelatedResourceConfigs + ?.revIncludeRelatedResourceConfigs(true) ?.groupBy { "${it.resource.name}_${it.searchParameter}".lowercase() } ?.mapValues { it.value.first() } @@ -557,7 +565,7 @@ constructor( resources = entry.value, relatedResourcesConfigsMap = fwdIncludedRelatedConfigsMap, relatedResourceWrapper = relatedResourceWrapper, - relatedResourcesQueue = relatedResourcesQueue + relatedResourcesQueue = relatedResourcesQueue, ) } searchResult.revIncluded?.forEach { entry -> @@ -569,7 +577,7 @@ constructor( resources = entry.value, relatedResourcesConfigsMap = revIncludedRelatedConfigsMap, relatedResourceWrapper = relatedResourceWrapper, - relatedResourcesQueue = relatedResourcesQueue + relatedResourcesQueue = relatedResourcesQueue, ) } } @@ -577,28 +585,77 @@ constructor( return relatedResourceWrapper } + private suspend fun computeCountForEachRelatedResource( + resources: List, + resourceConfig: ResourceConfig, + relatedResourceWrapper: RelatedResourceWrapper, + configComputedRuleValues: Map, + ) { + val relatedResourceCountLinkedList = LinkedList() + val key = resourceConfig.id ?: resourceConfig.resource.name + resources.forEach { baseResource -> + val search = + Search(type = resourceConfig.resource).apply { + filter( + ReferenceClientParam(resourceConfig.searchParameter), + { value = baseResource.logicalId.asReference(baseResource.resourceType).reference }, + ) + applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = false, + configComputedRuleValues = configComputedRuleValues, + ) + } + search.count( + onSuccess = { + relatedResourceCountLinkedList.add( + RelatedResourceCount( + relatedResourceType = resourceConfig.resource, + parentResourceId = baseResource.logicalId, + count = it, + ), + ) + }, + onFailure = { + Timber.e( + it, + "Error retrieving count for ${ + baseResource.logicalId.asReference( + baseResource.resourceType, + ) + } for related resource identified ID $key", + ) + }, + ) + } + + // Add each related resource count query result to map + relatedResourceWrapper.relatedResourceCountMap[key] = relatedResourceCountLinkedList + } + private fun updateResourceWrapperAndQueue( key: String, defaultKey: String?, resources: List, relatedResourcesConfigsMap: Map?, relatedResourceWrapper: RelatedResourceWrapper, - relatedResourcesQueue: ArrayDeque, List?>>) { - val resourceConfigs = relatedResourcesConfigsMap?.get(key) - val id = resourceConfigs?.id ?: defaultKey - if (!id.isNullOrBlank()) { - relatedResourceWrapper.relatedResourceMap[id] = - relatedResourceWrapper.relatedResourceMap.getOrPut(id) { mutableListOf() }.apply { - addAll(resources.distinctBy { it.logicalId }) - } - resources.chunked(COUNT) { item -> - with(resourceConfigs?.relatedResources) { - if (!this.isNullOrEmpty()) { - relatedResourcesQueue.addLast(Pair(item, this)) - } + relatedResourcesQueue: ArrayDeque, List?>>, + ) { + val resourceConfigs = relatedResourcesConfigsMap?.get(key) + val id = resourceConfigs?.id ?: defaultKey + if (!id.isNullOrBlank()) { + relatedResourceWrapper.relatedResourceMap[id] = + relatedResourceWrapper.relatedResourceMap + .getOrPut(id) { mutableListOf() } + .apply { addAll(resources.distinctBy { it.logicalId }) } + resources.chunked(DEFAULT_BATCH_SIZE) { item -> + with(resourceConfigs?.relatedResources) { + if (!this.isNullOrEmpty()) { + relatedResourcesQueue.addLast(Pair(item, this)) } } } + } } protected suspend fun Search.count( @@ -626,55 +683,55 @@ constructor( resources: List, configComputedRuleValues: Map, ): List> { - val search = - Search(resources.first().resourceType).apply { - val filters = - resources.map { - val apply: TokenParamFilterCriterion.() -> Unit = { value = of(it.logicalId) } - apply - } - filter(Resource.RES_ID, *filters.toTypedArray()) - } + val search = + Search(resources.first().resourceType).apply { + val filters = + resources.map { + val apply: TokenParamFilterCriterion.() -> Unit = { value = of(it.logicalId) } + apply + } + filter(Resource.RES_ID, *filters.toTypedArray()) + } - // Forward include related resources e.g. a member or managingEntity of a Group resource - val forwardIncludeResourceConfigs = - relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(false) + // Forward include related resources e.g. a member or managingEntity of a Group resource + val forwardIncludeResourceConfigs = + relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(false) - // Reverse include related resources e.g. all CarePlans, Immunizations for Patient resource - val reverseIncludeResourceConfigs = - relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(true) + // Reverse include related resources e.g. all CarePlans, Immunizations for Patient resource + val reverseIncludeResourceConfigs = + relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(true) - search.apply { - reverseIncludeResourceConfigs?.forEach { resourceConfig -> - revInclude( - resourceConfig.resource, - ReferenceClientParam(resourceConfig.searchParameter), - ) { - (this as Search).applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) - } + search.apply { + reverseIncludeResourceConfigs?.forEach { resourceConfig -> + revInclude( + resourceConfig.resource, + ReferenceClientParam(resourceConfig.searchParameter), + ) { + (this as Search).applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) } + } - forwardIncludeResourceConfigs?.forEach { resourceConfig -> - include( - resourceConfig.resource, - ReferenceClientParam(resourceConfig.searchParameter), - ) { - (this as Search).applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) - } + forwardIncludeResourceConfigs?.forEach { resourceConfig -> + include( + resourceConfig.resource, + ReferenceClientParam(resourceConfig.searchParameter), + ) { + (this as Search).applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) } } - return kotlin - .runCatching { fhirEngine.search(search) } - .onFailure { Timber.e(it, "Error fetching related resources") } - .getOrDefault(emptyList()) + } + return kotlin + .runCatching { fhirEngine.batchedSearch(search) } + .onFailure { Timber.e(it, "Error fetching related resources") } + .getOrDefault(emptyList()) } private fun List.revIncludeRelatedResourceConfigs(isRevInclude: Boolean) = @@ -719,7 +776,7 @@ constructor( configComputedRuleValues = computedValuesMap, ) } - val resources = fhirEngine.search(search).map { it.resource } + val resources = fhirEngine.batchedSearch(search).map { it.resource } val filteredResources = filterResourcesByFhirPathExpression( resourceFilterExpressions = eventWorkflow.resourceFilterExpressions, @@ -731,11 +788,12 @@ constructor( } resources.forEach { resource -> - val retrievedRelatedResources = retrieveRelatedResources( - resource = resource, - relatedResourcesConfigs = resourceConfig.relatedResources, - configComputedRuleValues = computedValuesMap, - ) + val retrievedRelatedResources = + retrieveRelatedResources( + resource = resource, + relatedResourcesConfigs = resourceConfig.relatedResources, + configComputedRuleValues = computedValuesMap, + ) retrievedRelatedResources.relatedResourceMap.forEach { resourcesMap -> val filteredRelatedResources = filterResourcesByFhirPathExpression( @@ -888,6 +946,81 @@ constructor( } } + suspend fun countResources( + filterByRelatedEntityLocation: Boolean, + baseResourceConfig: ResourceConfig, + filterActiveResources: List, + configComputedRuleValues: Map, + ) = + if (filterByRelatedEntityLocation) { + val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() + val locationIds = + syncLocationIds + .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } + .asSequence() + .flatten() + .toHashSet() + val countSearch = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + val totalCount = fhirEngine.count(countSearch) + var searchResultsCount = 0L + var pageNumber = 0 + var count = 0 + while (count < totalCount) { + val baseResourceSearch = + createSearch( + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + currentPage = pageNumber, + count = DEFAULT_BATCH_SIZE, + ) + searchResultsCount += + fhirEngine + .search(baseResourceSearch) + .asSequence() + .map { it.resource } + .filter { resource -> + when (resource.resourceType) { + ResourceType.Location -> locationIds.contains(resource.logicalId) + else -> + resource.meta.tag.any { + it.system == + context.getString(R.string.sync_strategy_related_entity_location_system) && + locationIds.contains(it.code) + } + } + } + .count() + .toLong() + count += DEFAULT_BATCH_SIZE + pageNumber++ + } + searchResultsCount + } else { + val search = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + search.count( + onFailure = { + Timber.e(it, "Error counting resources ${baseResourceConfig.resource.name}") + }, + ) + } + suspend fun searchResourcesRecursively( filterByRelatedEntityLocationMetaTag: Boolean, filterActiveResources: List?, @@ -906,48 +1039,52 @@ constructor( val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() val locationIds = syncLocationIds - .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId }} + .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } .flatten() .toHashSet() - val countSearch = - Search(baseResourceConfig.resource).apply { - applyConfiguredSortAndFilters( - resourceConfig = baseResourceConfig, - sortData = false, - filterActiveResources = filterActiveResources, - configComputedRuleValues = configComputedRuleValues, - ) - } - val totalCount = fhirEngine.count(countSearch) - val searchResults = ArrayDeque>() - var pageNumber = 0 - var count = 0 - while (count < totalCount) { - val baseResourceSearch = - createSearch( - baseResourceConfig = baseResourceConfig, - filterActiveResources = filterActiveResources, - configComputedRuleValues = configComputedRuleValues, - currentPage = pageNumber, - count = COUNT, - ) - val result = fhirEngine.search(baseResourceSearch) - searchResults.addAll(result.filter { searchResult -> + val countSearch = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + val totalCount = fhirEngine.count(countSearch) + val searchResults = ArrayDeque>() + var pageNumber = 0 + var count = 0 + while (count < totalCount) { + val baseResourceSearch = + createSearch( + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + currentPage = pageNumber, + count = DEFAULT_BATCH_SIZE, + ) + val result = fhirEngine.batchedSearch(baseResourceSearch) + searchResults.addAll( + result.filter { searchResult -> when (baseResourceConfig.resource) { - ResourceType.Location -> locationIds.contains(searchResult.resource.logicalId) - else -> searchResult.resource.meta.tag.any { - it.system == context.getString(R.string.sync_strategy_related_entity_location_system) - && locationIds.contains(it.code) + ResourceType.Location -> locationIds.contains(searchResult.resource.logicalId) + else -> + searchResult.resource.meta.tag.any { + it.system == + context.getString(R.string.sync_strategy_related_entity_location_system) && + locationIds.contains(it.code) } } - }) - count += COUNT - pageNumber++ - if (currentPage != null && pageSize != null) { - val maxPageCount = (currentPage + 1) * pageSize - if (searchResults.size >= maxPageCount) break - } + }, + ) + count += DEFAULT_BATCH_SIZE + pageNumber++ + if (currentPage != null && pageSize != null) { + val maxPageCount = (currentPage + 1) * pageSize + if (searchResults.size >= maxPageCount) break } + } if (currentPage != null && pageSize != null) { val fromIndex = currentPage * pageSize @@ -982,7 +1119,7 @@ constructor( currentPage = currentPage, count = pageSize, ) - fhirEngine.search(search) + fhirEngine.batchedSearch(search) } .onFailure { Timber.e( @@ -998,7 +1135,8 @@ constructor( filterActiveResources = filterActiveResources, baseResourceConfig = baseResourceConfig, ) - } as List + } + as List } } @@ -1132,16 +1270,16 @@ constructor( } private suspend fun retrieveSubLocations(locationId: String): ArrayDeque = - fhirEngine - .search( - Search(type = ResourceType.Location).apply { - filter( - Location.PARTOF, - { value = locationId.asReference(ResourceType.Location).reference }, - ) - }, - ) - .mapTo(ArrayDeque()) { it.resource } + fhirEngine + .batchedSearch( + Search(type = ResourceType.Location).apply { + filter( + Location.PARTOF, + { value = locationId.asReference(ResourceType.Location).reference }, + ) + }, + ) + .mapTo(ArrayDeque()) { it.resource } /** * A wrapper data class to hold search results. All related resources are flattened into one Map @@ -1149,11 +1287,12 @@ constructor( */ data class RelatedResourceWrapper( val relatedResourceMap: MutableMap> = mutableMapOf(), - val relatedResourceCountMap: MutableMap> = mutableMapOf(), + val relatedResourceCountMap: MutableMap> = + mutableMapOf(), ) companion object { - const val COUNT = 250 + const val DEFAULT_BATCH_SIZE = 250 const val SNOMED_SYSTEM = "http://hl7.org/fhir/R4B/valueset-condition-clinical.html" const val PATIENT_CONDITION_RESOLVED_CODE = "resolved" const val PATIENT_CONDITION_RESOLVED_DISPLAY = "Resolved" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt index 0363fc2061..35baead2f3 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt @@ -19,13 +19,10 @@ package org.smartregister.fhircore.engine.data.local.register import android.content.Context import ca.uhn.fhir.parser.IParser import com.google.android.fhir.FhirEngine -import com.google.android.fhir.datacapture.extensions.logicalId -import com.google.android.fhir.search.Search import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Resource -import org.hl7.fhir.r4.model.ResourceType -import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ConfigService @@ -41,10 +38,7 @@ import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid -import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationIds import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor -import timber.log.Timber -import javax.inject.Inject class RegisterRepository @Inject @@ -104,71 +98,12 @@ constructor( val configComputedRuleValues = registerConfiguration.configRules.configRulesComputedValues() val filterByRelatedEntityLocation = registerConfiguration.filterDataByRelatedEntityLocation val filterActiveResources = registerConfiguration.activeResourceFilters - if (filterByRelatedEntityLocation) { - val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() - val locationIds = - syncLocationIds - .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } - .asSequence() - .flatten() - .toHashSet() - val countSearch = - Search(baseResourceConfig.resource).apply { - applyConfiguredSortAndFilters( - resourceConfig = baseResourceConfig, - sortData = false, - filterActiveResources = filterActiveResources, - configComputedRuleValues = configComputedRuleValues, - ) - } - val totalCount = fhirEngine.count(countSearch) - var searchResultsCount = 0L - var pageNumber = 0 - var count = 0 - while (count < totalCount) { - val baseResourceSearch = - createSearch( - baseResourceConfig = baseResourceConfig, - filterActiveResources = filterActiveResources, - configComputedRuleValues = configComputedRuleValues, - currentPage = pageNumber, - count = COUNT, - ) - searchResultsCount += fhirEngine.search(baseResourceSearch) - .asSequence() - .map { it.resource } - .filter { resource -> - when (resource.resourceType) { - ResourceType.Location -> locationIds.contains(resource.logicalId) - else -> resource.meta.tag.any { - it.system == context.getString(R.string.sync_strategy_related_entity_location_system) - && locationIds.contains(it.code) - } - } - }.count().toLong() - count += COUNT - pageNumber++ - } - searchResultsCount - } else { - val search = - Search(baseResourceConfig.resource).apply { - applyConfiguredSortAndFilters( - resourceConfig = baseResourceConfig, - sortData = false, - filterActiveResources = registerConfiguration.activeResourceFilters, - configComputedRuleValues = configComputedRuleValues, - ) - } - search.count( - onFailure = { - Timber.e( - it, - "Error counting register data for register id: ${registerConfiguration.id}" - ) - }, - ) - } + countResources( + filterByRelatedEntityLocation = filterByRelatedEntityLocation, + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt index 0f82f47883..efa101b299 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt @@ -189,7 +189,9 @@ constructor( accountManager.peekAuthToken(account, AUTH_TOKEN_TYPE), ) Result.success(true) - } else Result.success(false) + } else { + Result.success(false) + } } catch (httpException: HttpException) { Result.failure(httpException) } catch (unknownHostException: UnknownHostException) { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt index e2c346fc93..1ce80b7bf7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt @@ -66,7 +66,9 @@ class NetworkModule { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY - } else HttpLoggingInterceptor.Level.BASIC + } else { + HttpLoggingInterceptor.Level.BASIC + } redactHeader(AUTHORIZATION) redactHeader(COOKIE) }, @@ -141,7 +143,9 @@ class NetworkModule { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY - } else HttpLoggingInterceptor.Level.BASIC + } else { + HttpLoggingInterceptor.Level.BASIC + } redactHeader(AUTHORIZATION) redactHeader(COOKIE) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt index 2893539540..474f2c0da8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt @@ -36,6 +36,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.isValidResourceType import org.smartregister.fhircore.engine.util.extension.resourceClassType import org.smartregister.p2p.model.RecordCount @@ -108,7 +109,7 @@ constructor( count = batchSize from = offset } - fhirEngine.search(search) + fhirEngine.batchedSearch(search) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt index 28aaf581ed..bcdb51f226 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt @@ -140,7 +140,9 @@ class HtmlPopulator( questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString { answer -> if (dateFormat == null) { answer.value.valueToString() - } else answer.value.valueToString(dateFormat) + } else { + answer.value.valueToString(dateFormat) + } } html.replace(i, matcher.end() + i, answer) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt index 7c1821d6b4..c03ff0a495 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt @@ -52,7 +52,9 @@ class ConfigRulesExecutor @Inject constructor(val fhirPathDataExtractor: FhirPat if (BuildConfig.DEBUG) { val timeToFireRules = measureTimeMillis { rulesEngine.fire(rules, facts) } Timber.d("Rule executed in $timeToFireRules millisecond(s)") - } else rulesEngine.fire(rules, facts) + } else { + rulesEngine.fire(rules, facts) + } return facts.get(DATA) as Map } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt index fce7444a95..6124b286ad 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt @@ -186,7 +186,9 @@ class ResourceDataRulesExecutor @Inject constructor(val rulesFactory: RulesFacto resources = newListRelatedResources, conditionalFhirPathExpression = listResource.conditionalFhirPathExpression, ) - } else newListRelatedResources ?: listOf() + } else { + newListRelatedResources ?: listOf() + } val sortConfig = listResource.sortConfig diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt index 9e563579fe..77a8e41935 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt @@ -140,7 +140,9 @@ constructor( if (BuildConfig.DEBUG) { val timeToFireRules = measureTimeMillis { rulesEngine.fire(rules, facts) } Timber.d("Rule executed in $timeToFireRules millisecond(s)") - } else rulesEngine.fire(rules, facts) + } else { + rulesEngine.fire(rules, facts) + } return facts.get(DATA) as Map } @@ -192,13 +194,14 @@ constructor( return if (referenceFhirPathExpression.isNullOrEmpty()) { value - } else + } else { value.filter { resource.logicalId == fhirPathDataExtractor .extractValue(it, referenceFhirPathExpression) .extractLogicalIdUuid() } + } } /** @@ -686,7 +689,9 @@ constructor( } if (createLocalChangeEntitiesAfterPurge) { defaultRepository.addOrUpdate(resource = updatedResource as Resource) - } else defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource)) + } else { + defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource)) + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt index 6fee7dfe88..55c2df7da2 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt @@ -23,7 +23,6 @@ import ca.uhn.fhir.util.TerserUtil import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get -import com.google.android.fhir.search.search import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Date import javax.inject.Inject @@ -60,6 +59,7 @@ import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.extension.addResourceParameter import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractFhirpathDuration import org.smartregister.fhircore.engine.util.extension.extractFhirpathPeriod @@ -120,7 +120,7 @@ constructor( // Only one CarePlan per plan, update or init a new one if not exists val output = fhirEngine - .search { + .batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = planDefinition.referenceValue() }, @@ -396,7 +396,9 @@ constructor( end = if (durationExpression.isNotBlank() && offsetDate.hasValue()) { evaluateToDate(offsetDate, "\$this + $durationExpression")?.value - } else carePlan.period.end + } else { + carePlan.period.end + } } .also { taskPeriods.add(it) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt index 0ff00e5ced..050173774b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.google.android.fhir.search.search import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.withContext @@ -33,6 +32,7 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.lastOffset import org.smartregister.fhircore.engine.util.getLastOffset @@ -94,7 +94,7 @@ constructor( suspend fun getCarePlans(batchSize: Int, lastOffset: Int) = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( CarePlan.STATUS, { value = of(CarePlan.CarePlanStatus.DRAFT.toCode()) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt index 9fbcf49923..fafba12498 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt @@ -21,7 +21,6 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get import com.google.android.fhir.search.filter.TokenParamFilterCriterion -import com.google.android.fhir.search.search import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Date import javax.inject.Inject @@ -39,6 +38,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.executionStartIsBeforeOrToday import org.smartregister.fhircore.engine.util.extension.expiredConcept import org.smartregister.fhircore.engine.util.extension.extractId @@ -66,7 +66,7 @@ constructor( Timber.i("Fetch and expire overdue tasks") val tasksResult = fhirEngine - .search { + .batchedSearch { filter( Task.STATUS, { value = of(TaskStatus.REQUESTED.toCoding()) }, @@ -148,7 +148,7 @@ constructor( val tasks = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( Task.STATUS, { value = of(TaskStatus.REQUESTED.toCoding()) }, @@ -235,7 +235,7 @@ constructor( suspend fun closeResourcesRelatedToCompletedServiceRequests() { Timber.i("Fetch completed service requests and close related resources") defaultRepository.fhirEngine - .search { + .batchedSearch { filter( ServiceRequest.STATUS, { value = of(ServiceRequest.ServiceRequestStatus.COMPLETED.toCode()) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt index 677cf7b786..b329d1a556 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt @@ -81,7 +81,9 @@ object AlertDialogue { dialog.findViewById(R.id.pr_circular)?.apply { if (alertIntent == AlertIntent.PROGRESS) { this.show() - } else this.hide() + } else { + this.hide() + } } dialog.findViewById(R.id.tv_alert_message)?.apply { this.text = message } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt index 5487b9d92e..7b6eb5619c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt @@ -114,7 +114,9 @@ fun PinInput( enteredPin = if (it.length < enteredPin.size) { enteredPin.safeRemoveLast() - } else enteredPin.safePlus(it.last()) + } else { + enteredPin.safePlus(it.last()) + } nextCellIndex = enteredPin.size onPinSet(enteredPin) onShowPinError(false) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt index 93a64f7537..fdddeae1ea 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt @@ -43,7 +43,6 @@ const val SEARCH_FOOTER_PREVIOUS_BUTTON_TAG = "searchFooterPreviousButtonTag" const val SEARCH_FOOTER_NEXT_BUTTON_TAG = "searchFooterNextButtonTag" const val SEARCH_FOOTER_PAGINATION_TAG = "searchFooterPaginationTag" - @Composable fun RegisterFooter( resultCount: Int, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt index 212ffcceaa..59bb1305d8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt @@ -87,7 +87,9 @@ fun MultiSelectCheckbox( imageVector = if (collapsedState.value) { Icons.Default.ArrowDropDown - } else Icons.AutoMirrored.Filled.ArrowRight, + } else { + Icons.AutoMirrored.Filled.ArrowRight + }, contentDescription = null, tint = Color.Gray, modifier = Modifier.clickable { collapsedState.value = !collapsedState.value }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt index 806075ad5b..7d873a8d40 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt @@ -31,11 +31,14 @@ import android.os.LocaleList import android.os.Parcelable import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.graphics.Color as ComposeColor import androidx.compose.ui.state.ToggleableState import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import java.io.Serializable +import java.util.Locale import kotlinx.coroutines.flow.firstOrNull import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore import org.smartregister.fhircore.engine.ui.theme.DangerColor @@ -46,9 +49,6 @@ import org.smartregister.fhircore.engine.ui.theme.SuccessColor import org.smartregister.fhircore.engine.ui.theme.WarningColor import org.smartregister.fhircore.engine.util.annotation.ExcludeFromJacocoGeneratedReport import timber.log.Timber -import java.io.Serializable -import java.util.Locale -import androidx.compose.ui.graphics.Color as ComposeColor const val ERROR_COLOR = "errorColor" const val PRIMARY_COLOR = "primaryColor" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt index af488ee20a..579f49c901 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt @@ -27,6 +27,6 @@ fun Bitmap.encodeToByteArray(): ByteArray { } } -fun ByteArray.decodeToBitmap(offset: Int = 0): Bitmap { - return BitmapFactory.decodeByteArray(this, offset, this.size) +fun ByteArray.decodeToBitmap(offset: Int = 0): Bitmap? { + return kotlin.runCatching { BitmapFactory.decodeByteArray(this, offset, this.size) }.getOrNull() } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt index 3688624ac4..ff4b014e10 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt @@ -18,9 +18,10 @@ package org.smartregister.fhircore.engine.util.extension import ca.uhn.fhir.util.UrlUtil import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get -import com.google.android.fhir.search.search +import com.google.android.fhir.search.Search import com.google.android.fhir.workflow.FhirOperator import org.hl7.fhir.r4.model.Composition import org.hl7.fhir.r4.model.IdType @@ -40,7 +41,7 @@ suspend inline fun FhirEngine.loadResource(resourceId: St } suspend fun FhirEngine.searchCompositionByIdentifier(identifier: String): Composition? = - this.search { + this.batchedSearch { filter(Composition.IDENTIFIER, { value = of(Identifier().apply { value = identifier }) }) } .map { it.resource } @@ -50,7 +51,9 @@ suspend fun FhirEngine.loadLibraryAtPath(fhirOperator: FhirOperator, path: Strin // resource path could be Library/123 OR something like http://fhir.labs.common/Library/123 val library = runCatching { get(IdType(path).idPart) }.getOrNull() - ?: search { filter(Library.URL, { value = path }) }.map { it.resource }.firstOrNull() + ?: batchedSearch { filter(Library.URL, { value = path }) } + .map { it.resource } + .firstOrNull() } suspend fun FhirEngine.loadLibraryAtPath( @@ -72,7 +75,7 @@ suspend fun FhirEngine.loadCqlLibraryBundle(fhirOperator: FhirOperator, measureP // resource path could be Measure/123 OR something like http://fhir.labs.common/Measure/123 val measure: Measure? = if (UrlUtil.isValid(measurePath)) { - search { filter(Measure.URL, { value = measurePath }) } + batchedSearch { filter(Measure.URL, { value = measurePath }) } .map { it.resource } .firstOrNull() } else { @@ -93,3 +96,29 @@ suspend fun FhirEngine.countUnSyncedResources() = .groupingBy { it.resourceType.spaceByUppercase() } .eachCount() .map { it.key to it.value } + +suspend fun FhirEngine.batchedSearch(search: Search) = + if (search.count != null) { + this.search(search) + } else { + val result = mutableListOf>() + var offset = search.from ?: 0 + val pageCount = 100 + do { + search.from = offset + search.count = pageCount + val searchResults = this.search(search) + result += searchResults + offset += searchResults.size + } while (searchResults.size == pageCount) + + result + } + +suspend inline fun FhirEngine.batchedSearch( + init: Search.() -> Unit, +): List> { + val search = Search(type = R::class.java.newInstance().resourceType) + search.init() + return this.batchedSearch(search) +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt index dd733712c4..32662559aa 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt @@ -92,7 +92,7 @@ fun MeasureReport.StratifierGroupComponent.findPercentage( ): String { return if (denominator == 0) { "0" - } else + } else { findPopulation(MeasurePopulationType.NUMERATOR) ?.count ?.toBigDecimal() @@ -103,6 +103,7 @@ fun MeasureReport.StratifierGroupComponent.findPercentage( reportConfiguration?.roundingStrategy?.value ?: DEFAULT_ROUNDING_STRATEGY.value, ) .toString() + } } val MeasureReport.StratifierGroupComponent.displayText @@ -165,5 +166,5 @@ suspend inline fun FhirEngine.retrievePreviouslyGeneratedMeasureReports( search.filter(MeasureReport.MEASURE, { value = measureUrl }) subjects.forEach { search.filter(MeasureReport.SUBJECT, { value = it }) } - return this.search(search).map { it.resource } + return this.batchedSearch(search).map { it.resource } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt index d036dfdde6..cbb925e223 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt @@ -183,7 +183,9 @@ fun List.prePopulateInitialValues( (it.value is Coding) && if (actionParam.value.contains(",")) { actionParam.value.split(",").contains((it.value as Coding).code) - } else actionParam.value == (it.value as Coding).code + } else { + actionParam.value == (it.value as Coding).code + } } .forEach { it.initialSelected = true } } else { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt index d833d7b9b0..c31a255128 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt @@ -25,10 +25,11 @@ fun Reference.extractId(): String = fun Reference.extractType(): ResourceType? = if (this.reference.isNullOrEmpty()) { null - } else + } else { this.reference.substringBefore("/" + this.extractId()).substringAfterLast("/").let { ResourceType.fromCode(it) } + } fun String.asReference(resourceType: ResourceType): Reference { val resourceId = this diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index 2f87eac395..a5642cad2b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -23,7 +23,12 @@ import ca.uhn.fhir.rest.gclient.ReferenceClientParam import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get -import com.google.android.fhir.search.search +import java.time.Duration +import java.time.temporal.ChronoUnit +import java.util.Date +import java.util.Locale +import java.util.UUID +import kotlin.math.abs import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BaseDateTimeType @@ -67,12 +72,6 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import timber.log.Timber -import java.time.Duration -import java.time.temporal.ChronoUnit -import java.util.Date -import java.util.Locale -import java.util.UUID -import kotlin.math.abs const val REFERENCE = "reference" const val PARTOF = "part-of" @@ -484,7 +483,7 @@ suspend fun Task.updateDependentTaskDueDate( return apply { val dependentTasks = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( referenceParameter = ReferenceClientParam(PARTOF), { value = id }, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index d9b0ceba8e..4bef2826de 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -1604,15 +1604,15 @@ class DefaultRepositoryTest : RobolectricTest() { val location1SubLocations = defaultRepository.retrieveFlattenedSubLocations(location1.logicalId) - Assert.assertEquals(4, location1SubLocations.size) - Assert.assertEquals(location2.logicalId, location1SubLocations[0].logicalId) - Assert.assertEquals(location3.logicalId, location1SubLocations[1].logicalId) - Assert.assertEquals(location4.logicalId, location1SubLocations[2].logicalId) - Assert.assertEquals(location5.logicalId, location1SubLocations[3].logicalId) + Assert.assertEquals(5, location1SubLocations.size) + Assert.assertEquals(location2.logicalId, location1SubLocations[1].logicalId) + Assert.assertEquals(location3.logicalId, location1SubLocations[2].logicalId) + Assert.assertEquals(location4.logicalId, location1SubLocations[3].logicalId) + Assert.assertEquals(location5.logicalId, location1SubLocations[4].logicalId) val location4SubLocations = defaultRepository.retrieveFlattenedSubLocations(location4.logicalId) - Assert.assertEquals(1, location4SubLocations.size) - Assert.assertEquals(location5.logicalId, location4SubLocations.first().logicalId) + Assert.assertEquals(2, location4SubLocations.size) + Assert.assertEquals(location5.logicalId, location4SubLocations.last().logicalId) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt index 589a5494c6..d276880a9a 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt @@ -28,7 +28,6 @@ import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.search.Search -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -117,6 +116,7 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.REFERENCE import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractId @@ -1784,7 +1784,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } val bundle = Bundle().apply { addEntry().resource = patient } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = "${PlanDefinition().fhirType()}/plandef-1" }, @@ -1836,7 +1836,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } val bundle = Bundle().apply { addEntry().resource = patient } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = "${PlanDefinition().fhirType()}/plandef-1" }, @@ -2088,7 +2088,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { input = listOf(Task.ParameterComponent(CodeableConcept(), StringType("9"))) } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = opv1.id.extractLogicalIdUuid() }, @@ -2104,7 +2104,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { ), ) coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = immunizationResource.id.extractLogicalIdUuid() }, @@ -2112,7 +2112,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } } returns listOf(SearchResult(resource = immunizationResource, null, null)) coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = encounter.id.extractLogicalIdUuid() }, diff --git a/android/geowidget/src/main/assets/conversion_config.json b/android/geowidget/src/main/assets/conversion_config.json deleted file mode 100644 index 8b16ae0668..0000000000 --- a/android/geowidget/src/main/assets/conversion_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Group": { - "name": "Group.name", - "family-id": "Group.id" - }, - "Location": { - } -} \ No newline at end of file diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt index f8b3300221..8cf247761c 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt @@ -76,99 +76,92 @@ class GeoWidgetFragment : Fragment() { internal var showCurrentLocationButton: Boolean = true internal var showPlaneSwitcherButton: Boolean = true internal var showAddLocationButton: Boolean = true - private lateinit var mapView: KujakuMapView - private lateinit var featureCollection: FeatureCollection - private var geoJsonSource: GeoJsonSource? = null + private var mapView: KujakuMapView? = null + private val mapFeatures = ArrayDeque() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN) - val view = setupViews() - mapView.onCreate(savedInstanceState) - return view + setUpMapView(savedInstanceState) + return LinearLayout(requireContext()).apply { + orientation = LinearLayout.VERTICAL + addView(mapView) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { geoWidgetViewModel.features.observe(viewLifecycleOwner) { result -> - zoomToLocationsOnMap(result.map { it.toFeature() }) + if (result.isNotEmpty()) { + // Limit features to be displayed to 1000 + if (mapFeatures.size <= MAP_FEATURES_LIMIT) { + mapFeatures.addAll(result.map { it.toFeature() }) + } + zoomMapWithFeatures() + } } } override fun onStart() { super.onStart() - mapView.onStart() + mapView?.onStart() } override fun onResume() { super.onResume() - mapView.onResume() + mapView?.onResume() } override fun onPause() { super.onPause() - mapView.onPause() + mapView?.onPause() } override fun onStop() { super.onStop() - mapView.onStop() + mapView?.onStop() } override fun onDestroy() { super.onDestroy() - mapView.onDestroy() + mapView?.onDestroy() } override fun onLowMemory() { super.onLowMemory() - mapView.onLowMemory() + mapView?.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - mapView.onSaveInstanceState(outState) + mapView?.onSaveInstanceState(outState) } - private fun setupViews(): LinearLayout { - mapView = setUpMapView() - featureCollection = - FeatureCollection.fromFeatures( - geoWidgetViewModel.features.value?.map { it.toFeature() } ?: listOf(), - ) - return LinearLayout(requireContext()).apply { - orientation = LinearLayout.VERTICAL - addView(mapView) - } - } - - private fun setUpMapView(): KujakuMapView { - return try { - KujakuMapView(requireActivity()).apply { - id = R.id.kujaku_widget - val builder = Style.Builder().fromUri(context.getString(R.string.style_map_fhir_core)) - getMapAsync { mapboxMap -> - mapboxMap.setStyle(builder) { style -> - geoJsonSource = style.getSourceAs(context.getString(R.string.data_set_quest)) - if (geoJsonSource != null) { - geoJsonSource!!.setGeoJson(featureCollection) + private fun setUpMapView(savedInstanceState: Bundle?) { + Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN) + mapView = + try { + KujakuMapView(requireActivity()).apply { + id = R.id.kujaku_widget + val builder = Style.Builder().fromUri(context.getString(R.string.style_map_fhir_core)) + getMapAsync { mapboxMap -> + mapboxMap.setStyle(builder) { style -> + addIconsLayer(style) + addMapStyle(style) } - addIconsLayer(style) - addMapStyle(style) } - } - if (showAddLocationButton) { - setOnAddLocationListener(this) + if (showAddLocationButton) { + setOnAddLocationListener(this) + } + setOnClickLocationListener(this) } - setOnClickLocationListener(this) + } catch (mapboxConfigurationException: MapboxConfigurationException) { + Timber.e(mapboxConfigurationException) + null } - } catch (e: MapboxConfigurationException) { - Timber.e(e) - mapView - } + mapView?.onCreate(savedInstanceState) } private fun addIconsLayer(mMapboxMapStyle: Style) { @@ -270,7 +263,11 @@ class GeoWidgetFragment : Fragment() { mapLayers.forEach { when (it.layer) { MapLayer.STREET -> addBaseLayer(MapBoxSatelliteLayer(), it.active) - MapLayer.SATELLITE -> addBaseLayer(StreetsBaseLayer(requireContext()), it.active) + MapLayer.SATELLITE -> + addBaseLayer( + StreetsBaseLayer(requireContext()), + it.active, + ) MapLayer.STREET_SATELLITE -> addBaseLayer(StreetSatelliteLayer(requireContext()), it.active) } @@ -317,25 +314,28 @@ class GeoWidgetFragment : Fragment() { ) } - private fun zoomToLocationsOnMap(features: List) { - if (features.isEmpty()) return - featureCollection = FeatureCollection.fromFeatures(features) - - val locationPoints = - featureCollection - .features() - ?.asSequence() - ?.filter { it.geometry() is Point } - ?.map { it.geometry() as Point } - ?.toMutableList() ?: emptyList() - - val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) - val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0) - val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) - val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50) - - geoJsonSource?.setGeoJson(featureCollection) - mapView.getMapAsync { mapboxMap -> mapboxMap.easeCamera(finalCameraPosition) } + private fun zoomMapWithFeatures() { + mapView?.getMapAsync { mapboxMap -> + val featureCollection = FeatureCollection.fromFeatures(mapFeatures.toList()) + val locationPoints = + featureCollection + .features() + ?.asSequence() + ?.filter { it.geometry() is Point } + ?.map { it.geometry() as Point } + ?.toMutableList() ?: emptyList() + + val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) + val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0) + val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) + val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50) + + with(mapboxMap) { + (style?.getSourceAs(requireContext().getString(R.string.data_set_quest)) as GeoJsonSource?) + ?.apply { setGeoJson(featureCollection) } + easeCamera(finalCameraPosition) + } + } } class Builder { @@ -392,6 +392,8 @@ class GeoWidgetFragment : Fragment() { } companion object { + const val MAP_FEATURES_LIMIT = 1000 + fun builder() = Builder() } } diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt index dc2073653f..c67a128404 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt @@ -16,19 +16,26 @@ package org.smartregister.fhircore.geowidget.screens +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.ServicePointType -import javax.inject.Inject @HiltViewModel class GeoWidgetViewModel @Inject constructor(val dispatcherProvider: DispatcherProvider) : ViewModel() { - val features = MutableLiveData>(mutableListOf()) + private val _features = MutableLiveData>(mutableListOf()) + val features: LiveData> + get() = _features + + fun submitFeatures(geoJsonFeatures: List) { + _features.postValue(geoJsonFeatures) + } fun getServicePointKeyToType(): Map { val map: MutableMap = HashMap() diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt index 2240510d7a..9c550cf424 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt @@ -14,42 +14,24 @@ * limitations under the License. */ -package org.smartregister.fhircore.geowidget.rule - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import org.junit.rules.TestRule +import org.junit.rules.TestWatcher import org.junit.runner.Description -import org.junit.runners.model.Statement -import org.smartregister.fhircore.engine.util.DispatcherProvider - -@ExperimentalCoroutinesApi -class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : - TestRule, TestCoroutineScope by TestCoroutineScope(testDispatcher) { - val testDispatcherProvider = - object : DispatcherProvider { - override fun default() = testDispatcher +@OptIn(ExperimentalCoroutinesApi::class) val testDispatcher = UnconfinedTestDispatcher() - override fun io() = testDispatcher - - override fun main() = testDispatcher +@ExperimentalCoroutinesApi +class CoroutineTestRule : TestWatcher() { - override fun unconfined() = testDispatcher - } + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } - override fun apply(base: Statement?, description: Description?) = - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - Dispatchers.setMain(testDispatcher) - base?.evaluate() - Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() - } - } + override fun finished(description: Description) { + Dispatchers.resetMain() + } } diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt index b84dcd5928..9713580412 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.geowidget.screens +import CoroutineTestRule import android.os.Build import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider @@ -48,7 +49,6 @@ import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.Geometry import org.smartregister.fhircore.geowidget.model.ServicePointType -import org.smartregister.fhircore.geowidget.rule.CoroutineTestRule @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class) @@ -63,6 +63,10 @@ class GeoWidgetViewModelTest { @Inject lateinit var configService: ConfigService + @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor + + @Inject lateinit var parser: IParser + private lateinit var configurationRegistry: ConfigurationRegistry private lateinit var sharedPreferencesHelper: SharedPreferencesHelper @@ -75,9 +79,6 @@ class GeoWidgetViewModelTest { private val configRulesExecutor: ConfigRulesExecutor = mockk() - @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor - - @Inject lateinit var parser: IParser private lateinit var viewModel: GeoWidgetViewModel @Mock private lateinit var dispatcherProvider: DispatcherProvider @@ -93,7 +94,7 @@ class GeoWidgetViewModelTest { spyk( DefaultRepository( fhirEngine = fhirEngine, - dispatcherProvider = coroutinesTestRule.testDispatcherProvider, + dispatcherProvider = dispatcherProvider, sharedPreferencesHelper = sharedPreferencesHelper, configurationRegistry = configurationRegistry, configService = configService, @@ -103,7 +104,7 @@ class GeoWidgetViewModelTest { context = ApplicationProvider.getApplicationContext(), ), ) - geoWidgetViewModel = spyk(GeoWidgetViewModel(coroutinesTestRule.testDispatcherProvider)) + geoWidgetViewModel = spyk(GeoWidgetViewModel(dispatcherProvider)) coEvery { defaultRepository.create(any()) } returns emptyList() } @@ -145,7 +146,7 @@ class GeoWidgetViewModelTest { serverVersion = serverVersion, ), ) - geoWidgetViewModel.features.value = geoJsonFeatures + geoWidgetViewModel.submitFeatures(geoJsonFeatures) Assert.assertEquals(geoWidgetViewModel.features.value!!.size, geoJsonFeatures.size) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 84f9530eee..12e8cf0182 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -47,7 +47,7 @@ junit-jupiter = "5.10.3" junit-ktx = "1.2.1" kotlin = "1.9.22" kotlin-serialization = "1.8.10" -kotlinx-coroutines = "1.8.1" +kotlinx-coroutines = "1.9.0" kotlinx-serialization-json = "1.6.0" kt3k-coveralls-ver="2.12.0" ktlint = "0.50.0" @@ -65,7 +65,7 @@ okhttp = "4.12.0" okhttp-logging-interceptor = "4.12.0" orchestrator = "1.5.0" owasp = "8.2.1" -p2p-lib = "0.6.10-SNAPSHOT" +p2p-lib = "0.6.11-SNAPSHOT" playServicesLocation = "21.3.0" playServicesTasks = "18.2.0" preference-ktx = "1.2.1" diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt index 8807b5d3e6..a565ea5d29 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt @@ -211,7 +211,9 @@ constructor( } if (migrationConfig.createLocalChangeEntitiesAfterPurge) { defaultRepository.addOrUpdate(resource = updatedResource as Resource) - } else defaultRepository.createRemote(resource = *arrayOf(updatedResource as Resource)) + } else { + defaultRepository.createRemote(resource = *arrayOf(updatedResource as Resource)) + } } } Timber.i("Data migration completed successfully for version: ${migrationConfig.version}") diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt new file mode 100644 index 0000000000..8e0217064b --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.data.geowidget + +import android.database.SQLException +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.google.android.fhir.datacapture.extensions.logicalId +import kotlinx.serialization.json.JsonPrimitive +import org.hl7.fhir.r4.model.Location +import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.data.local.register.RegisterRepository +import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor +import org.smartregister.fhircore.engine.util.extension.interpolate +import org.smartregister.fhircore.geowidget.model.GeoJsonFeature +import org.smartregister.fhircore.geowidget.model.Geometry +import timber.log.Timber + +/** [RegisterRepository] function for loading data to the paging source. */ +class GeoWidgetPagingSource( + private val defaultRepository: DefaultRepository, + private val resourceDataRulesExecutor: ResourceDataRulesExecutor, + private val geoWidgetConfig: GeoWidgetConfiguration, +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: 0 + val prevKey = if (currentPage > 0) currentPage - 1 else null + + val registerData = + defaultRepository.searchResourcesRecursively( + filterActiveResources = null, + fhirResourceConfig = geoWidgetConfig.resourceConfig, + configRules = null, + secondaryResourceConfigs = null, + filterByRelatedEntityLocationMetaTag = + geoWidgetConfig.filterDataByRelatedEntityLocation == true, + currentPage = currentPage, + pageSize = DEFAULT_PAGE_SIZE, + ) + + val nextKey = if (registerData.isNotEmpty()) currentPage + 1 else null + + val data = + registerData + .asSequence() + .filter { it.resource is Location } + .filter { (it.resource as Location).hasPosition() } + .filter { with((it.resource as Location).position) { hasLongitude() && hasLatitude() } } + .map { + Pair( + it.resource as Location, + resourceDataRulesExecutor.processResourceData( + repositoryResourceData = it, + ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), + params = emptyMap(), + ), + ) + } + .map { (location, resourceData) -> + GeoJsonFeature( + id = location.logicalId, + geometry = + Geometry( + coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) + listOf( + location.position.longitude.toDouble(), + location.position.latitude.toDouble(), + ), + ), + properties = + geoWidgetConfig.servicePointConfig?.servicePointProperties?.mapValues { + JsonPrimitive(it.value.interpolate(resourceData.computedValuesMap)) + } ?: emptyMap(), + ) + } + .toList() + LoadResult.Page(data = data, prevKey = prevKey, nextKey = nextKey) + } catch (exception: SQLException) { + Timber.e(exception) + LoadResult.Error(exception) + } catch (exception: Exception) { + Timber.e(exception) + LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + companion object { + const val DEFAULT_PAGE_SIZE = 20 + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt index 42e3ef14b7..f489eba575 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt @@ -66,8 +66,11 @@ class RegisterPagingSource( val prevKey = if (_registerPagingSourceState.loadAll && currentPage > 0) currentPage - 1 else null val nextKey = - if (_registerPagingSourceState.loadAll && registerData.isNotEmpty()) currentPage + 1 - else null + if (_registerPagingSourceState.loadAll && registerData.isNotEmpty()) { + currentPage + 1 + } else { + null + } val data = registerData.map { repositoryResourceData -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt index 03e1cd12fb..8ac0f9d169 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt @@ -172,13 +172,14 @@ constructor( entry.key, parentIt.map { it.focus.extractId() }, ) - } else + } else { fhirResourceDataSource.post( requestBody = generateRequestBundle(entry.key, parentIt.map { it.focus.extractId() }) .encodeResourceToString() .toRequestBody(NetworkModule.JSON_MEDIA_TYPE), ) + } resultBundle.entry.forEach { bundleEntryComponent -> if (bundleEntryComponent.resource != null) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt new file mode 100644 index 0000000000..0038324b78 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.ui.geowidget + +import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery + +sealed class GeoWidgetEvent { + data class SearchFeatures( + val searchQuery: SearchQuery = SearchQuery.emptyText, + val geoWidgetConfig: GeoWidgetConfiguration, + ) : GeoWidgetEvent() +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 948f7cafa1..4350b700f8 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -54,12 +54,12 @@ import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData -import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent import org.smartregister.fhircore.engine.ui.theme.AppTheme +import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment @@ -74,6 +74,7 @@ import org.smartregister.fhircore.quest.ui.main.AppMainUiState import org.smartregister.fhircore.quest.ui.main.AppMainViewModel import org.smartregister.fhircore.quest.ui.main.components.AppDrawer import org.smartregister.fhircore.quest.ui.shared.components.SnackBarMessage +import org.smartregister.fhircore.quest.ui.shared.models.SearchMode import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery import org.smartregister.fhircore.quest.ui.shared.viewmodels.SearchViewModel import org.smartregister.fhircore.quest.util.extensions.handleClickEvent @@ -90,6 +91,8 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { @Inject lateinit var configurationRegistry: ConfigurationRegistry + @Inject lateinit var dispatcherProvider: DispatcherProvider + private lateinit var geoWidgetFragment: GeoWidgetFragment private lateinit var geoWidgetConfiguration: GeoWidgetConfiguration private val navArgs by navArgs() @@ -104,7 +107,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { savedInstanceState: Bundle?, ): View { buildGeoWidgetFragment() - + geoWidgetLauncherViewModel.retrieveLocations(geoWidgetConfiguration, null) return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -174,23 +177,12 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { geoWidgetConfiguration = geoWidgetConfiguration, searchQuery = searchViewModel.searchQuery, search = { searchText -> - coroutineScope.launch { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchText, - ) - if (geoJsonFeatures.isNotEmpty()) { - geoWidgetViewModel.features.postValue(geoJsonFeatures) - } else { - geoWidgetLauncherViewModel.emitSnackBarState( - SnackBarMessageConfig( - message = - getString(R.string.no_found_locations_matching_text, searchText), - ), - ) - } - } + geoWidgetLauncherViewModel.onEvent( + GeoWidgetEvent.SearchFeatures( + searchQuery = SearchQuery(searchText, SearchMode.KeyboardInput), + geoWidgetConfig = geoWidgetConfiguration, + ), + ) }, isFirstTimeSync = geoWidgetLauncherViewModel.isFirstTime(), appDrawerUIState = appDrawerUIState, @@ -224,14 +216,10 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } is CurrentSyncJobStatus.Succeeded, is CurrentSyncJobStatus.Failed, -> { - lifecycleScope.launch { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, - ) - geoWidgetViewModel.features.postValue(geoJsonFeatures) - } + geoWidgetLauncherViewModel.retrieveLocations( + geoWidgetConfiguration, + searchViewModel.searchQuery.value.query, + ) appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) } else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) @@ -240,28 +228,27 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - showSetLocationDialog() - lifecycleScope.launch { - // Retrieve if searchText is null; filter will be triggered automatically if text is not empty - if (searchViewModel.searchQuery.value.isBlank()) { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, - ) - if (geoJsonFeatures.isNotEmpty()) { - geoWidgetViewModel.features.postValue(geoJsonFeatures) - } else { - geoWidgetLauncherViewModel.showNoLocationDialog(geoWidgetConfiguration) - } + geoWidgetLauncherViewModel.noLocationFoundDialog.observe(viewLifecycleOwner) { show -> + if (show) { + AlertDialogue.showAlert( + context = requireContext(), + alertIntent = AlertIntent.INFO, + message = geoWidgetConfiguration.noResults?.message!!, + title = geoWidgetConfiguration.noResults?.title!!, + confirmButtonListener = { + geoWidgetConfiguration.noResults + ?.actionButton + ?.actions + ?.handleClickEvent(findNavController()) + }, + confirmButtonText = R.string.positive_button_location_set, + cancellable = true, + neutralButtonListener = {}, + ) } } - setOnQuestionnaireSubmissionListener { - lifecycleScope.launch { - geoWidgetViewModel.features.postValue(geoWidgetViewModel.features.value?.plus(it)) - } - } + setOnQuestionnaireSubmissionListener { geoWidgetViewModel.submitFeatures(listOf(it)) } } override fun onPause() { @@ -320,6 +307,10 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { .showCurrentLocationButtonVisibility(geoWidgetConfiguration.showLocation) .setPlaneSwitcherButtonVisibility(geoWidgetConfiguration.showPlaneSwitcher) .build() + + lifecycleScope.launch { + geoWidgetLauncherViewModel.geoJsonFeatures.collect { geoWidgetViewModel.submitFeatures(it) } + } } private fun setOnQuestionnaireSubmissionListener(emitFeature: (GeoJsonFeature) -> Unit) { @@ -340,28 +331,4 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } } } - - private fun showSetLocationDialog() { - viewLifecycleOwner.lifecycleScope.launch { - geoWidgetLauncherViewModel.noLocationFoundDialog.observe(requireActivity()) { show -> - if (show) { - AlertDialogue.showAlert( - context = requireContext(), - alertIntent = AlertIntent.INFO, - message = geoWidgetConfiguration.noResults?.message!!, - title = geoWidgetConfiguration.noResults?.title!!, - confirmButtonListener = { - geoWidgetConfiguration.noResults - ?.actionButton - ?.actions - ?.handleClickEvent(findNavController()) - }, - confirmButtonText = R.string.positive_button_location_set, - cancellable = true, - neutralButtonListener = {}, - ) - } - } - } - } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt index 9c4d8d056f..12a32508c8 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt @@ -20,10 +20,14 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonPrimitive import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.IdType @@ -34,10 +38,10 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFilterConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType -import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider @@ -59,7 +63,6 @@ constructor( val resourceDataRulesExecutor: ResourceDataRulesExecutor, val configurationRegistry: ConfigurationRegistry, ) : ViewModel() { - private val _snackBarStateFlow = MutableSharedFlow() val snackBarStateFlow = _snackBarStateFlow.asSharedFlow() @@ -70,87 +73,106 @@ constructor( private val applicationConfiguration by lazy { configurationRegistry.retrieveConfiguration(ConfigType.Application) } - private lateinit var repositoryResourceDataList: List - - suspend fun retrieveLocations( - geoWidgetConfig: GeoWidgetConfiguration, - searchText: String? = null, - ): List { - val geoJsonFeatures = mutableListOf() - val repositoryResourceDataList = retrieveResources(geoWidgetConfig) - - repositoryResourceDataList.forEach { repositoryResourceData -> - val location = repositoryResourceData.resource as Location - val resourceData = - resourceDataRulesExecutor.processResourceData( - repositoryResourceData = repositoryResourceData, - ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), - params = emptyMap(), + + val geoJsonFeatures: MutableStateFlow> = MutableStateFlow(emptyList()) + + fun retrieveLocations(geoWidgetConfig: GeoWidgetConfiguration, searchText: String?) { + viewModelScope.launch(dispatcherProvider.io()) { + val totalCount = + defaultRepository.countResources( + filterByRelatedEntityLocation = geoWidgetConfig.filterDataByRelatedEntityLocation == true, + baseResourceConfig = geoWidgetConfig.resourceConfig.baseResource, + filterActiveResources = + listOf( + ActiveResourceFilterConfig( + resourceType = ResourceType.Patient, + active = true, + ), + ActiveResourceFilterConfig( + resourceType = ResourceType.Group, + active = true, + ), + ), + configComputedRuleValues = emptyMap(), ) - val servicePointProperties = mutableMapOf() - geoWidgetConfig.servicePointConfig?.servicePointProperties?.forEach { (key, value) -> - servicePointProperties[key] = - JsonPrimitive(value.interpolate(resourceData.computedValuesMap)) + if (totalCount == 0L) { + showNoLocationDialog(geoWidgetConfig) + return@launch } - - if ( - location.hasPosition() && - location.position.hasLatitude() && - location.position.hasLongitude() - ) { - val feature = - GeoJsonFeature( - id = location.idElement.idPart, - geometry = - Geometry( - coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) - listOf( - location.position.longitude.toDouble(), - location.position.latitude.toDouble(), + var count = 0 + var pageNumber = 0 + while (count < totalCount) { + val registerData = + defaultRepository + .searchResourcesRecursively( + filterActiveResources = null, + fhirResourceConfig = geoWidgetConfig.resourceConfig, + configRules = null, + secondaryResourceConfigs = null, + filterByRelatedEntityLocationMetaTag = + geoWidgetConfig.filterDataByRelatedEntityLocation == true, + currentPage = pageNumber, + pageSize = DefaultRepository.DEFAULT_BATCH_SIZE, + ) + .asSequence() + .filter { it.resource is Location } + .filter { (it.resource as Location).hasPosition() } + .filter { with((it.resource as Location).position) { hasLongitude() && hasLatitude() } } + .map { + Pair( + it.resource as Location, + resourceDataRulesExecutor.processResourceData( + repositoryResourceData = it, + ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), + params = emptyMap(), + ), + ) + } + .map { (location, resourceData) -> + GeoJsonFeature( + id = location.logicalId, + geometry = + Geometry( + coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) + listOf( + location.position.longitude.toDouble(), + location.position.latitude.toDouble(), + ), ), - ), - properties = servicePointProperties, - ) - - val keys = geoWidgetConfig.topScreenSection?.searchBar?.computedRules - - if (!keys.isNullOrEmpty() && !searchText.isNullOrEmpty()) { - val addFeature = - keys.any { key -> - servicePointProperties[key].toString().contains(other = searchText, ignoreCase = true) + properties = + geoWidgetConfig.servicePointConfig?.servicePointProperties?.mapValues { + JsonPrimitive(it.value.interpolate(resourceData.computedValuesMap)) + } ?: emptyMap(), + ) + } + .toList() + geoJsonFeatures.value = + if (searchText.isNullOrBlank()) { + registerData + } else { + registerData.filter { geoJsonFeature: GeoJsonFeature -> + geoWidgetConfig.topScreenSection?.searchBar?.computedRules?.any { ruleName -> + // if ruleName not found in map return {-1}; check always return false hence no data + val value = geoJsonFeature.properties[ruleName]?.toString() ?: "{-1}" + value.contains(other = searchText, ignoreCase = true) + } == true } - if (addFeature) { - geoJsonFeatures.add(feature) } - } - - if (searchText.isNullOrEmpty()) { - geoJsonFeatures.add(feature) - } + pageNumber++ + count += DefaultRepository.DEFAULT_BATCH_SIZE } } - return geoJsonFeatures } - fun showNoLocationDialog(geoWidgetConfiguration: GeoWidgetConfiguration) { - geoWidgetConfiguration.noResults?.let { _noLocationFoundDialog.postValue(true) } + fun onEvent(geoWidgetEvent: GeoWidgetEvent) { + when (geoWidgetEvent) { + is GeoWidgetEvent.SearchFeatures -> + retrieveLocations(geoWidgetEvent.geoWidgetConfig, geoWidgetEvent.searchQuery.query) + } } - suspend fun retrieveResources( - geoWidgetConfig: GeoWidgetConfiguration, - ): List { - if (!this::repositoryResourceDataList.isInitialized) { - repositoryResourceDataList = - defaultRepository.searchResourcesRecursively( - filterActiveResources = null, - fhirResourceConfig = geoWidgetConfig.resourceConfig, - configRules = null, - secondaryResourceConfigs = null, - filterByRelatedEntityLocationMetaTag = - geoWidgetConfig.filterDataByRelatedEntityLocation == true, - ) - } - return repositoryResourceDataList + fun showNoLocationDialog(geoWidgetConfiguration: GeoWidgetConfiguration) { + geoWidgetConfiguration.noResults?.let { _noLocationFoundDialog.postValue(true) } } suspend fun onQuestionnaireSubmission( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt index 966faf9bca..f873e96df6 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt @@ -90,7 +90,9 @@ open class LoginActivity : BaseMultiLanguageActivity() { downloadNowWorkflowConfigs() if (isPinEnabled && !hasActivePin) { navigateToPinLogin(launchSetup = true) - } else loginActivity.navigateToHome() + } else { + loginActivity.navigateToHome() + } } } launchDialPad.observe(loginActivity) { if (!it.isNullOrBlank()) launchDialPad(it) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt index f8e9363b3b..81c1f8371b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt @@ -199,7 +199,9 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, ), ), ) - } else Timber.e("QuestionnaireConfig & QuestionnaireResponse are both null") + } else { + Timber.e("QuestionnaireConfig & QuestionnaireResponse are both null") + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 04cae4f44c..0a7fce88db 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -34,6 +34,13 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import dagger.hilt.android.lifecycle.HiltViewModel +import java.text.SimpleDateFormat +import java.time.OffsetDateTime +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import javax.inject.Inject +import kotlin.time.Duration import kotlinx.coroutines.async import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -81,13 +88,6 @@ import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically -import java.text.SimpleDateFormat -import java.time.OffsetDateTime -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import javax.inject.Inject -import kotlin.time.Duration @HiltViewModel class AppMainViewModel @@ -134,14 +134,15 @@ constructor( } fun retrieveIconsAsBitmap() { - viewModelScope.launch (dispatcherProvider.io()){ + viewModelScope.launch(dispatcherProvider.io()) { navigationConfiguration.clientRegisters .asSequence() .filter { it.menuIconConfig != null && - it.menuIconConfig?.type == ICON_TYPE_REMOTE && - !it.menuIconConfig?.reference.isNullOrBlank() - }.mapNotNull { it.menuIconConfig!!.reference } + it.menuIconConfig?.type == ICON_TYPE_REMOTE && + !it.menuIconConfig?.reference.isNullOrBlank() + } + .mapNotNull { it.menuIconConfig!!.reference } .resourceReferenceToBitMap( fhirEngine = fhirEngine, decodedImageMap = configurationRegistry.decodedImageMap, @@ -291,11 +292,15 @@ constructor( NavigationArg.SCREEN_TITLE to if (startDestinationConfig.screenTitle.isNullOrEmpty()) { topMenuConfig.display - } else startDestinationConfig.screenTitle, + } else { + startDestinationConfig.screenTitle + }, NavigationArg.REGISTER_ID to if (startDestinationConfig.id.isNullOrEmpty()) { clickAction?.id ?: topMenuConfig.id - } else startDestinationConfig.id, + } else { + startDestinationConfig.id + }, ) } LauncherType.MAP -> bundleOf(NavigationArg.GEO_WIDGET_ID to startDestinationConfig.id) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt index 0ae4299164..2e2d6b3362 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt @@ -321,7 +321,9 @@ private fun DefaultSyncStatus( Modifier.background( if (allDataSynced) { SideMenuBottomItemDarkColor - } else WarningColor.copy(alpha = TRANSPARENCY), + } else { + WarningColor.copy(alpha = TRANSPARENCY) + }, ) .padding(vertical = 16.dp), ) { @@ -332,7 +334,9 @@ private fun DefaultSyncStatus( stringResource( if (allDataSynced) { org.smartregister.fhircore.engine.R.string.manual_sync - } else org.smartregister.fhircore.engine.R.string.sync, + } else { + org.smartregister.fhircore.engine.R.string.sync + }, ), subTitle = if (allDataSynced) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt index 64799c70b9..885990d23f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.smartregister.fhircore.engine.data.local.DefaultRepository @@ -36,7 +37,6 @@ import org.smartregister.fhircore.engine.ui.multiselect.TreeBuilder import org.smartregister.fhircore.engine.ui.multiselect.TreeNode import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor -import javax.inject.Inject @HiltViewModel class MultiSelectViewModel diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt index b82b1488d3..fa50c05d44 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt @@ -55,7 +55,9 @@ constructor( ): QuestionnaireResponse? { val searchQuery = createQuestionnaireResponseSearchQuery(questionnaireId, subjectId, subjectType) - return defaultRepository.search(searchQuery).firstOrNull() + return defaultRepository.search(searchQuery).maxByOrNull { + it.meta.lastUpdated + } } /** @@ -77,8 +79,6 @@ constructor( QuestionnaireResponse.QUESTIONNAIRE, { value = "${ResourceType.Questionnaire}/$questionnaireId" }, ) - count = 1 - from = 0 } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt index 1037a76430..04a946015a 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt @@ -163,7 +163,9 @@ fun ProfileScreen( bottom = if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { PADDING_BOTTOM_WITH_FAB.dp - } else PADDING_BOTTOM_WITHOUT_FAB.dp, + } else { + PADDING_BOTTOM_WITHOUT_FAB.dp + }, ), ) { item(key = profileUiState.resourceData?.baseResourceId) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt index 219cd3b6e5..1673f814d2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.db.ResourceNotFoundException import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -59,7 +60,6 @@ import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber -import javax.inject.Inject @HiltViewModel class ProfileViewModel @@ -149,7 +149,7 @@ constructor( withContext(dispatcherProvider.io()) { profileConfigs.views.decodeImageResourcesToBitmap( fhirEngine = registerRepository.fhirEngine, - decodedImageMap = configurationRegistry.decodedImageMap + decodedImageMap = configurationRegistry.decodedImageMap, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt index d7b6c97d24..229dc75593 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt @@ -154,7 +154,9 @@ private fun ChangeManagingEntityBottomBar( id = if (isEnabled) { org.smartregister.fhircore.engine.R.color.colorPrimary - } else org.smartregister.fhircore.engine.R.color.white, + } else { + org.smartregister.fhircore.engine.R.color.white + }, ), ), ) { @@ -165,7 +167,9 @@ private fun ChangeManagingEntityBottomBar( id = if (isEnabled) { org.smartregister.fhircore.engine.R.color.white - } else org.smartregister.fhircore.engine.R.color.colorPrimary, + } else { + org.smartregister.fhircore.engine.R.color.colorPrimary + }, ), text = stringResource(id = org.smartregister.fhircore.engine.R.string.str_save).uppercase(), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 50f1520793..030219472c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -33,9 +33,12 @@ import com.google.android.fhir.datacapture.validation.Valid import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.search.Search import com.google.android.fhir.search.filter.TokenParamFilterCriterion -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.Date +import java.util.UUID +import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -60,6 +63,7 @@ import org.smartregister.fhircore.engine.BuildConfig import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.GroupResourceConfig +import org.smartregister.fhircore.engine.configuration.LinkIdType import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.app.CodingSystemUsage @@ -77,6 +81,7 @@ import org.smartregister.fhircore.engine.util.extension.appendOrganizationInfo import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo import org.smartregister.fhircore.engine.util.extension.appendRelatedEntityLocation import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.clearText import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls @@ -95,10 +100,6 @@ import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.engine.util.helper.TransformSupportServices import org.smartregister.fhircore.quest.R import timber.log.Timber -import java.util.Date -import java.util.UUID -import javax.inject.Inject -import javax.inject.Provider @HiltViewModel class QuestionnaireViewModel @@ -786,7 +787,7 @@ constructor( if (libraryFilters.isNotEmpty()) { defaultRepository.fhirEngine - .search { + .batchedSearch { filter( Resource.RES_ID, *libraryFilters.toTypedArray(), @@ -1117,14 +1118,49 @@ constructor( null } + // Exclude the configured fields from QR + if (questionnaireResponse != null) { + val exclusionLinkIdsMap: Map = + questionnaireConfig.linkIds + ?.asSequence() + ?.filter { it.type == LinkIdType.PREPOPULATION_EXCLUSION } + ?.associateBy { it.linkId } + ?.mapValues { it.value.type == LinkIdType.PREPOPULATION_EXCLUSION } ?: emptyMap() + + questionnaireResponse.item = + excludePrepopulationFields(questionnaireResponse.item.toMutableList(), exclusionLinkIdsMap) + } return Pair(questionnaireResponse, launchContextResources) } + fun excludePrepopulationFields( + items: MutableList, + exclusionMap: Map, + ): MutableList { + val stack = LinkedList>() + stack.push(items) + while (stack.isNotEmpty()) { + val currentItems = stack.pop() + val iterator = currentItems.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (exclusionMap.containsKey(item.linkId)) { + iterator.remove() + } else if (item.item.isNotEmpty()) { + stack.push(item.item) + } + } + } + return items + } + private fun List.removeUnAnsweredItems(): List { - return this.filter { it.hasAnswer() || it.item.isNotEmpty() } + return this.asSequence() + .filter { it.hasAnswer() || it.item.isNotEmpty() } .onEach { it.item = it.item.removeUnAnsweredItems() } .filter { it.hasAnswer() || it.item.isNotEmpty() } + .toList() } /** diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index 601d529406..03d1bb1acf 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -46,6 +46,7 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.SyncOperation import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -68,7 +69,6 @@ import org.smartregister.fhircore.quest.ui.shared.viewmodels.SearchViewModel import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.hookSnackBar import org.smartregister.fhircore.quest.util.extensions.rememberLifecycleEvent -import javax.inject.Inject @ExperimentalMaterialApi @AndroidEntryPoint diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 5facb82ae5..54e4bf4e5d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -171,7 +171,9 @@ fun RegisterScreen( id = if (appDrawerUIState.isSyncUpload == true) { R.string.syncing_up - } else R.string.syncing_down, + } else { + R.string.syncing_down + }, ), showPercentageProgress = true, ) @@ -191,8 +193,9 @@ fun RegisterScreen( onEvent = onEvent, registerUiState = registerUiState, currentPage = currentPage, - showPagination = !registerUiState.registerConfiguration.infiniteScroll && - searchQuery.value.isBlank(), + showPagination = + !registerUiState.registerConfiguration.infiniteScroll && + searchQuery.value.isBlank(), onSearchByQrSingleResultAction = { resourceData -> if ( !searchQuery.value.isBlank() && searchQuery.value.mode == SearchMode.QrCodeScan diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt index 66834c58c0..1aa74b6290 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt @@ -29,6 +29,8 @@ import androidx.paging.cachedIn import androidx.paging.filter import com.google.android.fhir.sync.CurrentSyncJobStatus import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlin.math.ceil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -62,8 +64,6 @@ import org.smartregister.fhircore.quest.data.register.RegisterPagingSource import org.smartregister.fhircore.quest.data.register.model.RegisterPagingSourceState import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber -import javax.inject.Inject -import kotlin.math.ceil @HiltViewModel class RegisterViewModel @@ -154,8 +154,10 @@ constructor( return registerConfiguration } - private fun retrieveCompleteRegisterData(registerId: String, forceRefresh: Boolean): - Flow> { + private fun retrieveCompleteRegisterData( + registerId: String, + forceRefresh: Boolean, + ): Flow> { if (completeRegisterData == null || forceRefresh) { completeRegisterData = getPager(registerId, true).flow.cachedIn(viewModelScope) } @@ -171,24 +173,23 @@ constructor( val regConfig = retrieveRegisterConfiguration(registerId) if (regConfig.infiniteScroll) { registerData.value = retrieveCompleteRegisterData(registerId, false) - } else paginateRegisterData(registerId) + } else { + paginateRegisterData(registerId) + } } else { filterRegisterData(event.searchQuery.query) } } - is RegisterEvent.MoveToNextPage -> { currentPage.value = currentPage.value.plus(1) paginateRegisterData(registerId) } - is RegisterEvent.MoveToPreviousPage -> { currentPage.value.let { if (it > 0) currentPage.value = it.minus(1) } paginateRegisterData(registerId) } - RegisterEvent.ResetFilterRecordsCount -> _filteredRecordsCount.longValue = -1 - } + } } fun filterRegisterData(searchText: String) { @@ -456,7 +457,8 @@ constructor( viewModelScope.launch { val currentRegisterConfiguration = retrieveRegisterConfiguration(registerId, paramsMap) if (currentRegisterConfiguration.infiniteScroll) { - registerData.value = retrieveCompleteRegisterData(currentRegisterConfiguration.id, clearCache) + registerData.value = + retrieveCompleteRegisterData(currentRegisterConfiguration.id, clearCache) } else { _totalRecordsCount.longValue = registerRepository.countRegisterData(registerId = registerId, paramsMap = paramsMap) @@ -491,11 +493,11 @@ constructor( filteredRecordsCount = _filteredRecordsCount.longValue, pagesCount = ceil( - ( - if (registerFilterState.value.fhirResourceConfig != null) { - _filteredRecordsCount.longValue - } else _totalRecordsCount.longValue - ) + (if (registerFilterState.value.fhirResourceConfig != null) { + _filteredRecordsCount.longValue + } else { + _totalRecordsCount.longValue + }) .toDouble() .div(currentRegisterConfiguration.pageSize.toLong()), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt index a964f7a85a..194038b148 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt @@ -117,12 +117,17 @@ fun RegisterCardList( // Register pagination item { val fabActions = registerUiState.registerConfiguration?.fabActions - Box(modifier = Modifier.padding( - bottom = - if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { - PADDING_BOTTOM_WITH_FAB.dp - } else PADDING_BOTTOM_WITHOUT_FAB.dp, - )) { + Box( + modifier = + Modifier.padding( + bottom = + if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { + PADDING_BOTTOM_WITH_FAB.dp + } else { + PADDING_BOTTOM_WITHOUT_FAB.dp + }, + ), + ) { if (pagingItems.itemCount > 0 && showPagination) { RegisterFooter( resultCount = pagingItems.itemCount, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt index c6f272384c..3c0c481f40 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt @@ -25,7 +25,6 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf import com.google.android.fhir.FhirEngine -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -45,6 +44,7 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.extension.SDFHH_MM import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.firstDayOfMonth import org.smartregister.fhircore.engine.util.extension.formatDate import org.smartregister.fhircore.engine.util.extension.lastDayOfMonth @@ -78,7 +78,7 @@ constructor( Timber.w("started MeasureReportWorker") fhirEngine - .search {} + .batchedSearch {} .map { it.resource } .forEach { monthList?.forEachIndexed { index, date -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt index d7a4bcf438..468f58c079 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt @@ -92,9 +92,10 @@ internal class EditTextQrCodeItemViewHolderFactory( editable.toString().let { if (it.isBlank()) { null - } else + } else { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(StringType(it)) + } } qrCodeAnswerChangeListener.onQrCodeChanged( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt index 6e1c248eab..3bac623fb0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt @@ -60,7 +60,9 @@ object EditTextQrCodeViewHolderFactory : prevAnswerEmpty && !newAnswerEmpty -> { if (canHaveMultipleAnswers) { questionnaireViewItem.addAnswer(newAnswer!!) - } else questionnaireViewItem.setAnswer(newAnswer!!) + } else { + questionnaireViewItem.setAnswer(newAnswer!!) + } } !prevAnswerEmpty && newAnswerEmpty -> { questionnaireViewItem.removeAnswer(previousAnswer!!) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt index 77a7b46631..103b2795cd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt @@ -192,7 +192,9 @@ fun ActionableButton( } else { if (colorOpacity == 0.0f) { DefaultColor.copy(alpha = 0.9f) - } else statusColor.copy(alpha = colorOpacity) + } else { + statusColor.copy(alpha = colorOpacity) + } }, textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 2783138e71..423323e660 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -106,7 +106,9 @@ fun SyncBottomBar( val bottomRadius = if (!hideSyncCompleteStatus.value || currentSyncJobStatus is CurrentSyncJobStatus.Running) { 32.dp - } else 0.dp + } else { + 0.dp + } val height = when { syncNotificationBarExpanded -> @@ -147,7 +149,9 @@ fun SyncBottomBar( imageVector = if (syncNotificationBarExpanded) { Icons.Default.KeyboardArrowDown - } else Icons.Default.KeyboardArrowUp, + } else { + Icons.Default.KeyboardArrowUp + }, contentDescription = null, tint = when (currentSyncJobStatus) { @@ -237,7 +241,9 @@ fun SyncStatusView( imageVector = if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { Icons.Default.CheckCircle - } else Icons.Default.Error, + } else { + Icons.Default.Error + }, contentDescription = null, tint = when (currentSyncJobStatus) { @@ -308,7 +314,9 @@ fun SyncStatusView( stringResource( if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { org.smartregister.fhircore.engine.R.string.retry - } else org.smartregister.fhircore.engine.R.string.cancel, + } else { + org.smartregister.fhircore.engine.R.string.cancel + }, ), modifier = Modifier.padding(start = 16.dp).clickable { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt index 8855bb2021..276d67c769 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt @@ -58,7 +58,9 @@ fun ViewRenderer( val interpolatedProperties = if (areViewPropertiesInterpolated) { properties - } else properties.interpolate(resourceData.computedValuesMap) + } else { + properties.interpolate(resourceData.computedValuesMap) + } GenerateView( modifier = generateModifier(properties), properties = interpolatedProperties, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index 0fc6c394bc..df61fa4bff 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -30,6 +30,7 @@ import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.NavOptions import com.google.android.fhir.FhirEngine +import kotlin.collections.set import org.hl7.fhir.r4.model.Binary import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig @@ -62,7 +63,6 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.ui.pdf.PdfLauncherFragment import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler import org.smartregister.p2p.utils.startP2PScreen -import kotlin.collections.set const val PRACTITIONER_ID = "practitionerId" @@ -184,16 +184,23 @@ fun ActionConfig.handleClickEvent( navController.navigate(MainNavigationScreen.Insight.route) ApplicationWorkflow.DEVICE_TO_DEVICE_SYNC -> startP2PScreen(navController.context) ApplicationWorkflow.LAUNCH_MAP -> { - val mapFragmentDestination = MainNavigationScreen.GeoWidgetLauncher.route - - val isMapFragmentExists = navController.currentDestination?.id == mapFragmentDestination - if (isMapFragmentExists) { - navController.popBackStack(mapFragmentDestination, false) + val args = bundleOf(NavigationArg.GEO_WIDGET_ID to actionConfig.id) + // If value != null, we are navigating FROM a map; disallow same map navigation + val currentGeoWidgetId = + navController.currentBackStackEntry?.arguments?.getString(NavigationArg.GEO_WIDGET_ID) + val sameGeoWidgetNavigation = + args.getString(NavigationArg.GEO_WIDGET_ID) == + navController.previousBackStackEntry?.arguments?.getString(NavigationArg.GEO_WIDGET_ID) + if (!currentGeoWidgetId.isNullOrEmpty() && sameGeoWidgetNavigation) { + return } else { navController.navigate( - resId = mapFragmentDestination, - args = bundleOf(NavigationArg.GEO_WIDGET_ID to actionConfig.id), - navOptions = navOptions(mapFragmentDestination, inclusive = true, singleOnTop = true), + resId = MainNavigationScreen.GeoWidgetLauncher.route, + args = args, + navOptions = + navController.currentDestination?.id?.let { + navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack == true) + }, ) } } @@ -261,12 +268,12 @@ suspend fun Sequence.resourceReferenceToBitMap( fhirEngine: FhirEngine, decodedImageMap: SnapshotStateMap, ) { - forEach { - val resourceId = it.extractLogicalIdUuid() - fhirEngine.loadResource(resourceId)?.let { binary -> - decodedImageMap[resourceId] = binary.data.decodeToBitmap() - } + forEach { + val resourceId = it.extractLogicalIdUuid() + fhirEngine.loadResource(resourceId)?.let { binary -> + binary.data.decodeToBitmap()?.let { bitmap -> decodedImageMap[resourceId] = bitmap } } + } } suspend fun List.decodeImageResourcesToBitmap( @@ -276,28 +283,31 @@ suspend fun List.decodeImageResourcesToBitmap( val queue = ArrayDeque(this) while (queue.isNotEmpty()) { val viewProperty = queue.removeFirst() - when(viewProperty.viewType) { + when (viewProperty.viewType) { ViewType.IMAGE -> { val imageProperties = (viewProperty as ImageProperties) if (imageProperties.imageConfig != null) { val imageConfig = imageProperties.imageConfig - if (ICON_TYPE_REMOTE.equals(imageConfig?.type, ignoreCase = true) && - !imageConfig?.reference.isNullOrBlank()) { + if ( + ICON_TYPE_REMOTE.equals(imageConfig?.type, ignoreCase = true) && + !imageConfig?.reference.isNullOrBlank() + ) { val resourceId = imageConfig!!.reference!! fhirEngine.loadResource(resourceId)?.let { binary: Binary -> - decodedImageMap[resourceId] = binary.data.decodeToBitmap() + binary.data.decodeToBitmap()?.let { bitmap -> decodedImageMap[resourceId] = bitmap } } } } } ViewType.COLUMN -> (viewProperty as ColumnProperties).children.forEach(queue::addLast) - ViewType.ROW -> (viewProperty as RowProperties).children.forEach(queue::addLast) - ViewType.SERVICE_CARD -> (viewProperty as ServiceCardProperties).details.forEach(queue::addLast) + ViewType.ROW -> (viewProperty as RowProperties).children.forEach(queue::addLast) + ViewType.SERVICE_CARD -> + (viewProperty as ServiceCardProperties).details.forEach(queue::addLast) ViewType.CARD -> (viewProperty as CardViewProperties).content.forEach(queue::addLast) ViewType.LIST -> (viewProperty as ListProperties).registerCard.views.forEach(queue::addLast) ViewType.STACK -> (viewProperty as StackViewProperties).children.forEach(queue::addLast) else -> { - /**Ignore other views that cannot display images**/ + /** Ignore other views that cannot display images* */ } } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt index d9724ab580..14a356e110 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt @@ -220,7 +220,9 @@ class CqlContentTest : RobolectricTest() { it.name to if (it.hasResource()) { it.resource.encodeResourceToString() - } else it.valueToString() + } else { + it.valueToString() + } } val expectedResource = resource.parseSampleResourceFromFile().convertToString(true) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt index c7da70b671..f1009313e7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt @@ -16,15 +16,15 @@ package org.smartregister.fhircore.quest.event +import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import kotlin.test.assertEquals -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlin.test.assertTrue import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before import org.junit.Rule @@ -38,39 +38,42 @@ class EventBusTest : RobolectricTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) - @Inject lateinit var eventQueue: EventQueue - private lateinit var eventBus: EventBus - lateinit var emittedEvents: MutableList + @Inject lateinit var eventBus: EventBus @Before fun setUp() { hiltRule.inject() - emittedEvents = mutableListOf() - eventBus = EventBus(eventQueue) } @Test - @OptIn(ExperimentalCoroutinesApi::class) fun testTriggerEventEmitsLogoutEvent1() { - val onSubmitQuestionnaireEvent = - AppEvent.OnSubmitQuestionnaire( - QuestionnaireSubmission( - questionnaireConfig = QuestionnaireConfig(id = "submit-questionnaire"), - QuestionnaireResponse(), - ), - ) + runTest { + val onSubmitQuestionnaireEvent = + AppEvent.OnSubmitQuestionnaire( + QuestionnaireSubmission( + questionnaireConfig = QuestionnaireConfig(id = "questionnaire1"), + questionnaireResponse = QuestionnaireResponse().apply { id = "questionnaireResponse1" }, + ), + ) - runBlockingTest { - val collectJob = launch { + val job = eventBus.events - .getFor("TestTag") - .onEach { appEvent -> emittedEvents.add(appEvent) } + .getFor("thisConsumer") + .onEach { + assertTrue(it is AppEvent.OnSubmitQuestionnaire) + assertEquals( + onSubmitQuestionnaireEvent.questionnaireSubmission.questionnaireConfig.id, + it.questionnaireSubmission.questionnaireConfig.id, + ) + assertEquals( + onSubmitQuestionnaireEvent.questionnaireSubmission.questionnaireResponse.logicalId, + it.questionnaireSubmission.questionnaireResponse.logicalId, + ) + } .launchIn(this) - } + eventBus.triggerEvent(onSubmitQuestionnaireEvent) - collectJob.cancel() + job.cancel() } - - assertEquals(onSubmitQuestionnaireEvent, emittedEvents[0]) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/pdf/PdfLauncherViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/pdf/PdfLauncherViewModelTest.kt new file mode 100644 index 0000000000..0bba57aa0d --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/pdf/PdfLauncherViewModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.quest.pdf + +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult +import com.google.android.fhir.search.Search +import io.mockk.coEvery +import io.mockk.mockk +import java.util.Date +import kotlinx.coroutines.test.runTest +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.ResourceType +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.yesterday +import org.smartregister.fhircore.quest.robolectric.RobolectricTest +import org.smartregister.fhircore.quest.ui.pdf.PdfLauncherViewModel + +class PdfLauncherViewModelTest : RobolectricTest() { + + private lateinit var fhirEngine: FhirEngine + private lateinit var defaultRepository: DefaultRepository + private lateinit var viewModel: PdfLauncherViewModel + + @Before + fun setUp() { + fhirEngine = mockk() + defaultRepository = + DefaultRepository( + fhirEngine = fhirEngine, + dispatcherProvider = mockk(), + sharedPreferencesHelper = mockk(), + configurationRegistry = mockk(), + configService = mockk(), + configRulesExecutor = mockk(), + fhirPathDataExtractor = mockk(), + parser = mockk(), + context = mockk(), + ) + viewModel = PdfLauncherViewModel(defaultRepository) + } + + @Test + fun testRetrieveQuestionnaireResponseReturnsLatestResponse() = runTest { + val patient = Patient().apply { id = "p1" } + val questionnaire = Questionnaire().apply { id = "q1" } + val olderQuestionnaireResponse = + QuestionnaireResponse().apply { + id = "qr2" + meta.lastUpdated = yesterday() + subject = patient.asReference() + setQuestionnaire(questionnaire.asReference().reference) + } + val latestQuestionnaireResponse = + QuestionnaireResponse().apply { + id = "qr1" + meta.lastUpdated = Date() + subject = patient.asReference() + setQuestionnaire(questionnaire.asReference().reference) + } + val questionnaireResponses = + listOf(olderQuestionnaireResponse, latestQuestionnaireResponse).map { + SearchResult(it, null, null) + } + + coEvery { fhirEngine.search(any()) } returns + questionnaireResponses + val result = + viewModel.retrieveQuestionnaireResponse(questionnaire.id, patient.id, ResourceType.Patient) + + assertEquals(latestQuestionnaireResponse.id, result!!.id) + } +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt index e2a47a82cf..0ec21168c4 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt @@ -34,7 +34,6 @@ import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before @@ -143,18 +142,9 @@ class GeoWidgetLauncherViewModelTest : RobolectricTest() { @Test fun testRetrieveLocationsShouldReturnGeoJsonFeatureList() { runTest { - val geoJsonFeatures = viewModel.retrieveLocations(geoWidgetConfiguration) - assertTrue(geoJsonFeatures.isNotEmpty()) - assertEquals("loc1", geoJsonFeatures.first().id) - } - } - - @Test - fun testRetrieveResourcesShouldReturnListOfRepositoryResourceData() { - runTest { - val retrieveResources = viewModel.retrieveResources(geoWidgetConfiguration) - assertFalse(retrieveResources.isEmpty()) - assertEquals("loc1", retrieveResources.first().resource.logicalId) + viewModel.retrieveLocations(geoWidgetConfiguration, null) + assertTrue(viewModel.geoJsonFeatures.value.isNotEmpty()) + assertEquals("loc1", viewModel.geoJsonFeatures.value.first().id) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index d0c93b6fdc..88f62221e5 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -163,7 +163,6 @@ class QuestionnaireViewModelTest : RobolectricTest() { @ExperimentalCoroutinesApi fun setUp() { hiltRule.inject() - // Write practitioner and organization to shared preferences sharedPreferencesHelper.write( SharedPreferenceKey.PRACTITIONER_ID.name, @@ -1840,6 +1839,92 @@ class QuestionnaireViewModelTest : RobolectricTest() { Assert.assertTrue(initialValueDate.isToday) } + @Test + fun testThatPopulateQuestionnaireSetInitialDefaultValueButExcludesFieldFromResponse() = + runTest(timeout = 90.seconds) { + val thisQuestionnaireConfig = + questionnaireConfig.copy( + resourceType = ResourceType.Patient, + resourceIdentifier = patient.logicalId, + type = QuestionnaireType.EDIT.name, + linkIds = + listOf( + LinkIdConfig("dateToday", LinkIdType.PREPOPULATION_EXCLUSION), + ), + ) + val questionnaireViewModelInstance = + QuestionnaireViewModel( + defaultRepository = defaultRepository, + dispatcherProvider = defaultRepository.dispatcherProvider, + fhirCarePlanGenerator = fhirCarePlanGenerator, + resourceDataRulesExecutor = resourceDataRulesExecutor, + transformSupportServices = mockk(), + sharedPreferencesHelper = sharedPreferencesHelper, + fhirOperator = fhirOperator, + fhirValidatorProvider = fhirValidatorProvider, + fhirPathDataExtractor = fhirPathDataExtractor, + configurationRegistry = configurationRegistry, + ) + val questionnaireWithDefaultDate = + Questionnaire().apply { + id = thisQuestionnaireConfig.id + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "dateToday" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension( + Extension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + Expression().apply { + language = "text/fhirpath" + expression = "today()" + }, + ), + ) + }, + ) + } + + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "dateToday" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(Date()) + }, + ) + }, + ) + setQuestionnaire( + thisQuestionnaireConfig.id.asReference(ResourceType.Questionnaire).reference, + ) + } + + coEvery { + fhirEngine.get( + thisQuestionnaireConfig.resourceType!!, + thisQuestionnaireConfig.resourceIdentifier!!, + ) + } returns patient + + coEvery { fhirEngine.search(any()) } returns + listOf( + SearchResult(questionnaireResponse, included = null, revIncluded = null), + ) + + val (result, _) = + questionnaireViewModelInstance.populateQuestionnaire( + questionnaire = questionnaireWithDefaultDate, + questionnaireConfig = thisQuestionnaireConfig, + actionParameters = emptyList(), + ) + + Assert.assertNotNull(result?.item) + Assert.assertTrue(result!!.item.isEmpty()) + } + @Test fun testThatPopulateQuestionnaireReturnsQuestionnaireResponseWithUnAnsweredRemoved() = runTest { val questionnaireViewModelInstance = @@ -1947,4 +2032,44 @@ class QuestionnaireViewModelTest : RobolectricTest() { Assert.assertNotNull(result.first) Assert.assertTrue(result.first!!.find("linkid-1") == null) } + + @Test + fun testExcludeNestedItemFromQuestionnairePrepopulation() { + val item1 = QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "1" } + val item2 = QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "2" } + val item3 = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3" + item = + mutableListOf( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "3.1" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2" + item = + mutableListOf( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2.1" + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2.2" + }, + ) + }, + ) + } + + val items = mutableListOf(item1, item2, item3) + val exclusionMap = mapOf("2" to true, "3.1" to true, "3.2.2" to true) + val filteredItems = questionnaireViewModel.excludePrepopulationFields(items, exclusionMap) + Assert.assertEquals(2, filteredItems.size) + Assert.assertEquals("1", filteredItems.first().linkId) + val itemThree = filteredItems.last() + Assert.assertEquals("3", itemThree.linkId) + Assert.assertEquals(1, itemThree.item.size) + val itemThreePointTwo = itemThree.item.first() + Assert.assertEquals("3.2", itemThreePointTwo.linkId) + Assert.assertEquals(1, itemThreePointTwo.item.size) + val itemThreePointTwoOne = itemThreePointTwo.item.first() + Assert.assertEquals("3.2.1", itemThreePointTwoOne.linkId) + } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt index 55f632705b..a8e7bf5ee7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt @@ -28,6 +28,7 @@ import io.mockk.mockk import io.mockk.runs import io.mockk.spyk import io.mockk.verify +import javax.inject.Inject import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DateType @@ -59,7 +60,6 @@ import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery -import javax.inject.Inject @HiltAndroidTest class RegisterViewModelTest : RobolectricTest() { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt index db2020c2ba..f0ae3ae24c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt @@ -37,8 +37,11 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.ResourceType @@ -72,6 +75,7 @@ import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation import org.smartregister.fhircore.engine.domain.model.ViewType +import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker @@ -79,7 +83,6 @@ import org.smartregister.fhircore.quest.navigation.MainNavigationScreen import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler -import javax.inject.Inject @HiltAndroidTest class ConfigExtensionsKtTest : RobolectricTest() { @@ -95,6 +98,8 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Inject lateinit var fhirEngine: FhirEngine + @Inject lateinit var dispatcherProvider: DispatcherProvider + private val navController = mockk(relaxUnitFun = true, relaxed = true) private val context = mockk(relaxUnitFun = true, relaxed = true) private val navigationMenuConfig by lazy { @@ -672,10 +677,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegistersDoneCorrectly(): Unit = runBlocking { - defaultRepository.create(addResourceTags = true, binaryImage) - val navigationMenuConfigs = sequenceOf(navigationMenuConfig).mapNotNull { it.menuIconConfig?.reference } + val navigationMenuConfigs = + sequenceOf(navigationMenuConfig).mapNotNull { it.menuIconConfig?.reference } val decodedImageMap = mutableStateMapOf() - runBlocking { + withContext(dispatcherProvider.io()) { + defaultRepository.create(addResourceTags = true, binaryImage) navigationMenuConfigs.resourceReferenceToBitMap( fhirEngine = fhirEngine, decodedImageMap = decodedImageMap, @@ -687,12 +693,14 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun decodeBinaryResourcesToBitmapOnOverflowMenuConfigDoneCorrectly(): Unit = runTest { - defaultRepository.create(addResourceTags = true, binaryImage) val navigationMenuConfigs = sequenceOf(overflowMenuItemConfig).mapNotNull { it.icon?.reference } val decodedImageMap = mutableStateMapOf() - runBlocking { - navigationMenuConfigs.resourceReferenceToBitMap( fhirEngine = fhirEngine, - decodedImageMap = decodedImageMap,) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + navigationMenuConfigs.resourceReferenceToBitMap( + fhirEngine = fhirEngine, + decodedImageMap = decodedImageMap, + ) } Assert.assertTrue(decodedImageMap.isNotEmpty()) Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) @@ -700,9 +708,12 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenProfileConfiguration(): Unit = runTest { - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - profileConfiguration.views.decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + fhirEngine.create(binaryImage) + profileConfiguration.views.decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } + Assert.assertTrue(decodedImageMap.isNotEmpty()) Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } @@ -710,9 +721,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenCardViewProperties(): Unit = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - listOf(cardViewProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(cardViewProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -720,9 +733,12 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenListViewProperties(): Unit = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - listOf(cardViewProperties.content[0]).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(cardViewProperties.content[0]) + .decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -732,8 +748,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties val decodedImageMap = mutableStateMapOf() - defaultRepository.create(addResourceTags = true, binaryImage) - listOf(listViewProperties.registerCard.views[0]).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(listViewProperties.registerCard.views[0]) + .decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -743,9 +762,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties val columnProperties = listViewProperties.registerCard.views[0] as ColumnProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - listOf(columnProperties.children[0]).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(columnProperties.children[0]).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -769,12 +790,13 @@ class ConfigExtensionsKtTest : RobolectricTest() { ), ) val decodedImageMap = mutableStateMapOf() - listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.isEmpty()) Assert.assertTrue(!decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } - @Test(expected = Exception::class) fun testExceptionCaughtOnDecodingBitmap() = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties @@ -787,7 +809,7 @@ class ConfigExtensionsKtTest : RobolectricTest() { imageConfig = ImageConfig( type = ICON_TYPE_REMOTE, - reference = "imageReference", + reference = "null Reference", ), ), ), @@ -796,13 +818,15 @@ class ConfigExtensionsKtTest : RobolectricTest() { coEvery { defaultRepository.loadResource(anyString()) } returns Binary().apply { - this.id = "imageReference" + this.id = "null Reference" this.contentType = "image/jpeg" this.data = "gibberish value".toByteArray() } - listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + withContext(Dispatchers.IO) { + listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.isEmpty()) - Assert.assertTrue(!decodedImageMap.containsKey("imageReference")) + Assert.assertFalse(decodedImageMap.containsKey("null Reference")) } } diff --git a/docs/engineering/app/configuring/forms/forms.mdx b/docs/engineering/app/configuring/forms/forms.mdx index 0b31ed5f3f..f5d4878e95 100644 --- a/docs/engineering/app/configuring/forms/forms.mdx +++ b/docs/engineering/app/configuring/forms/forms.mdx @@ -441,3 +441,19 @@ The QR code widget supports adding an arbitrary number of QR codes, implemented } ``` The extension's implementation can be found [here](https://github.com/opensrp/fhircore/blob/main/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt) + +## Excluding questionnaire fields from prepopulation + +Use the `linkIds` property to provide linkIds for the Questionnaire fields that should not be pre-field with data during editing or when opening the questionnaire in a read only format. +The `LinkIdType` required for the exclusion to work is `PREPOPULATION_EXCLUSION`. Nested fields can also be excluded from pre-population of forms. + +Example: + +```json +"linkIds": [ + { + "linkId": "ad29c7bd-8041-427f-8e63-b066afe5b438-009", + "type": "PREPOPULATION_EXCLUSION" + } +] +``` diff --git a/package-lock.json b/package-lock.json index e2721f2799..72013f6fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4146,9 +4146,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -4158,7 +4158,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5681,9 +5681,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -5987,36 +5987,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -6057,9 +6057,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", @@ -6244,12 +6244,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8529,9 +8529,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10471,9 +10474,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11617,11 +11623,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12655,9 +12661,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -12690,6 +12696,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12802,14 +12816,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" diff --git a/yarn.lock b/yarn.lock index 4352176802..25545d361e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2704,10 +2704,10 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -2717,7 +2717,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3713,6 +3713,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -3918,36 +3923,36 @@ execa@^5.0.0: strip-final-newline "^2.0.0" express@^4.17.3: - version "4.19.2" - resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -4042,13 +4047,13 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -5526,10 +5531,10 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -6402,10 +6407,10 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-to-regexp@2.2.1: version "2.2.1" @@ -6829,12 +6834,12 @@ pupa@^3.1.0: dependencies: escape-goat "^4.0.0" -qs@6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" queue-microtask@^1.2.2: version "1.2.3" @@ -7420,10 +7425,10 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -7473,15 +7478,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-function-length@^1.2.1: version "1.2.2" @@ -7543,9 +7548,9 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -side-channel@^1.0.4: +side-channel@^1.0.6: version "1.0.6" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: call-bind "^1.0.7"