Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into main-validate-extract…
Browse files Browse the repository at this point in the history
…ed-resources
  • Loading branch information
LZRS committed Jun 20, 2024
2 parents 452efa7 + 742fdeb commit e4c0821
Show file tree
Hide file tree
Showing 28 changed files with 934 additions and 369 deletions.
7 changes: 3 additions & 4 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ plugins {
alias(libs.plugins.org.owasp.dependencycheck)
alias(libs.plugins.com.diffplug.spotless) apply false
alias(libs.plugins.android.junit5) apply false

}

tasks.dokkaHtmlMultiModule {
Expand All @@ -35,21 +34,21 @@ tasks.dokkaHtmlMultiModule {
}
}

apply(from = "mapbox.gradle.kts")

allprojects {
repositories {
gradlePluginPortal()
mavenLocal()
google()
mavenCentral()
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots")
maven(url = "https://jcenter.bintray.com/")
apply(plugin = "org.owasp.dependencycheck")
tasks.dependencyCheckAggregate{
dependencyCheck.formats.add("XML")
}
configurations.all{
resolutionStrategy.force ("com.google.android.gms:play-services-location:19.0.1")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.event.EventWorkflow
import org.smartregister.fhircore.engine.domain.model.ActionConfig
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.DataQuery
import org.smartregister.fhircore.engine.domain.model.QuestionnaireType
import org.smartregister.fhircore.engine.domain.model.ResourceFilterExpression
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig
import org.smartregister.fhircore.engine.domain.model.SortConfig
import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid
import org.smartregister.fhircore.engine.util.extension.interpolate

Expand Down Expand Up @@ -60,6 +63,7 @@ data class QuestionnaireConfig(
val showRequiredTextAsterisk: Boolean = true,
val showRequiredText: Boolean = false,
val managingEntityRelationshipCode: String? = null,
val uniqueIdAssignment: UniqueIdAssignmentConfig? = null,
val linkIds: List<LinkIdConfig>? = null,
) : java.io.Serializable, Parcelable {

Expand Down Expand Up @@ -90,6 +94,8 @@ data class QuestionnaireConfig(
onSubmitActions = onSubmitActions?.map { it.interpolate(computedValuesMap) },
barcodeLinkId = barcodeLinkId?.interpolate(computedValuesMap),
cqlInputResources = cqlInputResources?.map { it.interpolate(computedValuesMap) },
uniqueIdAssignment =
uniqueIdAssignment?.copy(linkId = uniqueIdAssignment.linkId.interpolate(computedValuesMap)),
linkIds = linkIds?.onEach { it.linkId.interpolate(computedValuesMap) },
saveButtonText = saveButtonText?.interpolate(computedValuesMap),
)
Expand Down Expand Up @@ -120,6 +126,30 @@ data class ExtractedResourceUniquePropertyExpression(
val fhirPathExpression: String,
) : java.io.Serializable, Parcelable

@Serializable
@Parcelize
/**
* @property linkId The linkId used to capture the OpenSRP unique ID in the Questionnaire. Typically
* a GUID.
* @property idFhirPathExpression The FHIR Path expression for extracting ID from the configured
* [resource]
* @property resource The type of resource used to store generated IDs
* @property readOnly Whether to disable/enable editing of link ID.
* @property dataQueries The queries used to filter resources used for representing OpenSRP unique
* IDs.
* @property sortConfigs Configuration for sorting resources
* @property resourceFilterExpression Expression used to filter the returned resources
*/
data class UniqueIdAssignmentConfig(
val linkId: String,
val idFhirPathExpression: String,
val readOnly: Boolean = true,
val resource: ResourceType,
val dataQueries: List<DataQuery> = emptyList(),
val sortConfigs: List<SortConfig>? = null,
val resourceFilterExpression: ResourceFilterExpression? = null,
) : java.io.Serializable, Parcelable

@Serializable
@Parcelize
data class LinkIdConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ data class ApplicationConfiguration(
),
val logGpsLocation: List<LocationLogOptions> = emptyList(),
val usePractitionerAssignedLocationOnSync: Boolean = true,
val navigationStartDestination: LauncherType = LauncherType.REGISTER,
val navigationStartDestination: NavigationStartDestinationConfig =
NavigationStartDestinationConfig(
launcherType = LauncherType.REGISTER,
id = null,
),
val codingSystems: List<CodingSystemConfig> = emptyList(),
) : Configuration()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.configuration.app

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.smartregister.fhircore.engine.domain.model.LauncherType

/**
* This class configures the initial screen the application will be directed to upon launch. The
* application currently supports [LauncherType.REGISTER] and [LauncherType.MAP] screen as the entry
* point. This config defaults to launching a register with the rest of the properties obtained from
* the first NavigationItemConfig of the NavigationConfiguration
*/
@Serializable
@Parcelize
data class NavigationStartDestinationConfig(
val id: String? = null,
val screenTitle: String? = null,
val launcherType: LauncherType = LauncherType.REGISTER,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ data class GeoWidgetConfiguration(
override var appId: String,
override var configType: String = ConfigType.GeoWidget.name,
val id: String,
val profileId: String,
val topScreenSection: TopScreenSectionConfig? = null,
val registrationQuestionnaire: QuestionnaireConfig,
val mapLayers: List<MapLayerConfig> = listOf(MapLayerConfig(MapLayer.STREET, true)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.get
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.filter.ReferenceParamFilterCriterion
import com.google.android.fhir.search.filter.TokenParamFilterCriterion
Expand Down Expand Up @@ -69,11 +70,11 @@ 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.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.configuration.event.EventWorkflow
import org.smartregister.fhircore.engine.configuration.profile.ManagingEntityConfig
import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFilterConfig
import org.smartregister.fhircore.engine.data.local.register.RegisterRepository
import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore
import org.smartregister.fhircore.engine.domain.model.Code
import org.smartregister.fhircore.engine.domain.model.DataQuery
Expand Down Expand Up @@ -155,8 +156,8 @@ constructor(
suspend inline fun count(search: Search) = fhirEngine.count(search)

/**
* Saves a resource in the database. It also updates the [Resource.meta.lastUpdated] and generates
* the [Resource.id] if it is missing before saving the resource.
* Saves a resource in the database. It also updates the [Resource.meta] _lastUpdated and
* generates the [Resource.id] if it is missing before saving the resource.
*
* By default, mandatory Resource tags for sync are added but this can be disabled through the
* param [addResourceTags]
Expand Down Expand Up @@ -428,7 +429,7 @@ constructor(
) {
val activeResource = filterActiveResources?.find { it.resourceType == resourceConfig.resource }
if (!filterActiveResources.isNullOrEmpty() && activeResource?.active == true) {
filter(TokenClientParam(RegisterRepository.ACTIVE), { value = of(true) })
filter(TokenClientParam(ACTIVE), { value = of(true) })
}

resourceConfig.dataQueries?.forEach { dataQuery ->
Expand Down Expand Up @@ -839,7 +840,7 @@ constructor(
* @param resources - The list of resources to be filtered. Note that it only contains resources
* of a single type.
*/
fun filterResourcesByFhirPathExpression(
private fun filterResourcesByFhirPathExpression(
resourceFilterExpressions: List<ResourceFilterExpression>?,
resources: List<Resource>,
): List<Resource> {
Expand Down Expand Up @@ -921,7 +922,7 @@ constructor(
withContext(dispatcherProvider.io()) { fhirEngine.update(updatedResource as Resource) }
}

fun getJsonContent(jsonElement: JsonElement): Any? {
private fun getJsonContent(jsonElement: JsonElement): Any? {
return when (jsonElement) {
is JsonPrimitive -> jsonElement.jsonPrimitive.content
is JsonObject -> jsonElement.jsonObject
Expand Down Expand Up @@ -1096,6 +1097,49 @@ constructor(
}
}

suspend fun retrieveUniqueIdAssignmentResource(
uniqueIdAssignmentConfig: UniqueIdAssignmentConfig?,
): Resource? {
if (uniqueIdAssignmentConfig != null) {
val search =
Search(uniqueIdAssignmentConfig.resource).apply {
uniqueIdAssignmentConfig.dataQueries.forEach {
filterBy(dataQuery = it, configComputedRuleValues = emptyMap())
}
if (uniqueIdAssignmentConfig.sortConfigs != null) {
sort(uniqueIdAssignmentConfig.sortConfigs)
} else {
sort(
DateClientParam(LAST_UPDATED),
Order.DESCENDING,
)
}
}

val resources = search<Resource>(search)
val idResources =
if (uniqueIdAssignmentConfig.resourceFilterExpression != null) {
resources.filter { resource ->
val (conditionalFhirPathExpressions, matchAll) =
uniqueIdAssignmentConfig.resourceFilterExpression
if (matchAll) {
conditionalFhirPathExpressions.all {
fhirPathDataExtractor.extractValue(resource, it).toBoolean()
}
} else {
conditionalFhirPathExpressions.any {
fhirPathDataExtractor.extractValue(resource, it).toBoolean()
}
}
}
} else {
resources
}
return idResources.firstOrNull()
}
return null
}

/**
* A wrapper data class to hold search results. All related resources are flattened into one Map
* including the nested related resources as required by the Rules Engine facts.
Expand All @@ -1110,5 +1154,7 @@ constructor(
const val PATIENT_CONDITION_RESOLVED_CODE = "resolved"
const val PATIENT_CONDITION_RESOLVED_DISPLAY = "Resolved"
const val TAG = "_tag"
const val LAST_UPDATED = "_lastUpdated"
const val ACTIVE = "active"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,4 @@ constructor(
paramsMap: Map<String, String>?,
): RegisterConfiguration =
configurationRegistry.retrieveConfiguration(ConfigType.Register, registerId, paramsMap)

companion object {
const val ACTIVE = "active"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.hl7.fhir.r4.model.Address
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.CarePlan
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
Expand Down Expand Up @@ -76,6 +77,7 @@ import org.junit.Test
import org.smartregister.fhircore.engine.app.AppConfigService
import org.smartregister.fhircore.engine.app.fakes.Faker
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.configuration.event.EventWorkflow
import org.smartregister.fhircore.engine.configuration.event.UpdateWorkflowValueConfig
Expand Down Expand Up @@ -1418,4 +1420,69 @@ class DefaultRepositoryTest : RobolectricTest() {

coVerify { fhirEngine.create(resource, isLocalOnly = true) }
}

@Test
fun testRetrieveUniqueIdAssignmentResourceShouldReturnAResource() =
runTest(timeout = 30.seconds) {
val group1 =
Group().apply {
id = "grp1"
addCharacteristic(
Group.GroupCharacteristicComponent(
CodeableConcept().apply { text = "phn" },
CodeableConcept().apply { text = "1234" },
BooleanType(true),
),
)
addCharacteristic(
Group.GroupCharacteristicComponent(
CodeableConcept().apply { text = "phn" },
CodeableConcept().apply { text = "123456" },
BooleanType(false),
),
)
active = true
type = Group.GroupType.DEVICE
name = "Unique IDs"
}

val group2 =
Group().apply {
id = "grp2"
addCharacteristic(
Group.GroupCharacteristicComponent(
CodeableConcept().apply { text = "phn" },
CodeableConcept().apply { text = "56789" },
BooleanType(false),
),
)
active = false
type = Group.GroupType.DEVICE
name = "Unique IDs"
}

val uniqueIdAssignmentConfig =
UniqueIdAssignmentConfig(
linkId = "phn",
idFhirPathExpression =
"Group.characteristic.where(exclude=false and code.text='phn').first().value.text",
readOnly = false,
resource = ResourceType.Group,
resourceFilterExpression =
ResourceFilterExpression(
conditionalFhirPathExpressions =
listOf(
"Group.active = true and Group.type = 'device' and Group.name = 'Unique IDs'",
),
matchAll = true,
),
)

fhirEngine.create(group1, group2)
val resource = defaultRepository.retrieveUniqueIdAssignmentResource(uniqueIdAssignmentConfig)
Assert.assertNotNull(resource)
Assert.assertTrue(resource is Group)
Assert.assertEquals("1234", (resource as Group).characteristic[0].valueCodeableConcept.text)
Assert.assertFalse(resource.characteristic[1].exclude)
}
}
Loading

0 comments on commit e4c0821

Please sign in to comment.