Skip to content

Commit

Permalink
Allow users to request referenced resources along with the searched v…
Browse files Browse the repository at this point in the history
…ia inline params. (#1978)

* Inital changes to support inline resources via revInclude

* Updated test and added doc

* RevInclude to return mapped response

* Test code for forward include

* Fixed include query

* Unify search api to include include and revInclude functionality

* Workflow library: Incorporated changes related to FhirEngine.search

* Review comments: Changed the search result type to include the search param as well.

* Updated docs

* Added spotless toggle to skip indentation for specific code portions

* Added individual tests for include and revInclude

* Review Comments: Updated kdocs and refactored function name

* Review comments: refactored type of revInclude

* Fixed failing workflow compilation as workflow doesn't depends on engine project directly but released .aar

* Resolved compilation errors

* Reverted the engine changes
  • Loading branch information
aditya-07 committed Aug 21, 2023
1 parent 7f79e26 commit b7b8b98
Show file tree
Hide file tree
Showing 18 changed files with 1,209 additions and 247 deletions.
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/SpotlessConfig.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -36,6 +36,7 @@ fun Project.configureSpotless() {
// It is necessary to tell spotless the top level of a file in order to apply config to it
// See: https://github.com/diffplug/spotless/issues/135
)
toggleOffOn()
}
kotlinGradle {
target("*.gradle.kts")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class FhirApplication : Application(), DataCaptureConfig.Provider {
dataCaptureConfig =
DataCaptureConfig().apply {
urlResolver = ReferenceUrlResolver(this@FhirApplication as Context)
xFhirQueryResolver = XFhirQueryResolver { fhirEngine.search(it) }
xFhirQueryResolver = XFhirQueryResolver { fhirEngine.search(it).map { it.resource } }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.get
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.revInclude
import com.google.android.fhir.search.search
import java.text.SimpleDateFormat
import java.time.LocalDate
Expand All @@ -40,6 +40,8 @@ import org.apache.commons.lang3.StringUtils
import org.hl7.fhir.r4.model.Condition
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.RiskAssessment
import org.hl7.fhir.r4.model.codesystems.RiskProbability

Expand All @@ -59,108 +61,131 @@ class PatientDetailsViewModel(
viewModelScope.launch { livePatientData.value = getPatientDetailDataModel() }
}

private suspend fun getPatient(): PatientListViewModel.PatientItem {
val patient = fhirEngine.get<Patient>(patientId)
return patient.toPatientItem(0)
}
private suspend fun getPatientDetailDataModel(): List<PatientDetailData> {
val searchResult =
fhirEngine.search<Patient> {
filter(Resource.RES_ID, { value = of(patientId) })

private suspend fun getPatientObservations(): List<PatientListViewModel.ObservationItem> {
val observations: MutableList<PatientListViewModel.ObservationItem> = mutableListOf()
fhirEngine
.search<Observation> { filter(Observation.SUBJECT, { value = "Patient/$patientId" }) }
.take(MAX_RESOURCE_COUNT)
.map { createObservationItem(it, getApplication<Application>().resources) }
.let { observations.addAll(it) }
return observations
}
revInclude<RiskAssessment>(RiskAssessment.SUBJECT)
revInclude<Observation>(Observation.SUBJECT)
revInclude<Condition>(Condition.SUBJECT)
}
val data = mutableListOf<PatientDetailData>()

private suspend fun getPatientConditions(): List<PatientListViewModel.ConditionItem> {
val conditions: MutableList<PatientListViewModel.ConditionItem> = mutableListOf()
fhirEngine
.search<Condition> { filter(Condition.SUBJECT, { value = "Patient/$patientId" }) }
.take(MAX_RESOURCE_COUNT)
.map { createConditionItem(it, getApplication<Application>().resources) }
.let { conditions.addAll(it) }
return conditions
}
searchResult.first().let {
data.addPatientDetailData(
it.resource,
getRiskItem(
it.revIncluded?.get(ResourceType.RiskAssessment to RiskAssessment.SUBJECT.paramName)
as List<RiskAssessment>?
)
)

private suspend fun getPatientDetailDataModel(): List<PatientDetailData> {
val data = mutableListOf<PatientDetailData>()
val patient = getPatient()
patient.riskItem = getPatientRiskAssessment()
it.revIncluded?.get(ResourceType.Observation to Observation.SUBJECT.paramName)?.let {
data.addObservationsData(it as List<Observation>)
}
it.revIncluded?.get(ResourceType.Condition to Condition.SUBJECT.paramName)?.let {
data.addConditionsData(it as List<Condition>)
}
}
return data
}

val observations = getPatientObservations()
val conditions = getPatientConditions()
private fun getRiskItem(riskAssessments: List<RiskAssessment>?) =
riskAssessments
?.filter { it.hasOccurrence() }
?.maxByOrNull { it.occurrenceDateTimeType.value }
.let {
RiskAssessmentItem(
getRiskAssessmentStatusColor(it),
getRiskAssessmentStatus(it),
getLastContactedDate(it),
getPatientDetailsCardColor(it)
)
}

patient.let { patientItem ->
data.add(PatientDetailOverview(patientItem, firstInGroup = true))
data.add(
PatientDetailProperty(
PatientProperty(getString(R.string.patient_property_mobile), patientItem.phone)
private fun MutableList<PatientDetailData>.addPatientDetailData(
patient: Patient,
riskAssessment: RiskAssessmentItem
) {
patient
.toPatientItem(0)
.apply { riskItem = riskAssessment }
.let { patientItem ->
add(PatientDetailOverview(patientItem, firstInGroup = true))
add(
PatientDetailProperty(
PatientProperty(getString(R.string.patient_property_mobile), patientItem.phone)
)
)
)
data.add(
PatientDetailProperty(
PatientProperty(getString(R.string.patient_property_id), patientItem.resourceId)
add(
PatientDetailProperty(
PatientProperty(getString(R.string.patient_property_id), patientItem.resourceId)
)
)
)
data.add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_address),
"${patientItem.city}, ${patientItem.country} "
add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_address),
"${patientItem.city}, ${patientItem.country} "
)
)
)
)
data.add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_dob),
patientItem.dob?.localizedString ?: ""
add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_dob),
patientItem.dob?.localizedString ?: ""
)
)
)
)
data.add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_gender),
patientItem.gender.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
}
),
lastInGroup = true
add(
PatientDetailProperty(
PatientProperty(
getString(R.string.patient_property_gender),
patientItem.gender.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
}
),
lastInGroup = true
)
)
)
}
}
}

private fun MutableList<PatientDetailData>.addObservationsData(observations: List<Observation>) {
if (observations.isNotEmpty()) {
data.add(PatientDetailHeader(getString(R.string.header_observation)))
add(PatientDetailHeader(getString(R.string.header_observation)))

val observationDataModel =
observations.mapIndexed { index, observationItem ->
observations
.take(MAX_RESOURCE_COUNT)
.map { createObservationItem(it, getApplication<Application>().resources) }
.mapIndexed { index, observationItem ->
PatientDetailObservation(
observationItem,
firstInGroup = index == 0,
lastInGroup = index == observations.size - 1
)
}
data.addAll(observationDataModel)
.let { addAll(it) }
}
}

private fun MutableList<PatientDetailData>.addConditionsData(conditions: List<Condition>) {
if (conditions.isNotEmpty()) {
data.add(PatientDetailHeader(getString(R.string.header_conditions)))
val conditionDataModel =
conditions.mapIndexed { index, conditionItem ->
add(PatientDetailHeader(getString(R.string.header_conditions)))
conditions
.take(MAX_RESOURCE_COUNT)
.map { createConditionItem(it, getApplication<Application>().resources) }
.mapIndexed { index, conditionItem ->
PatientDetailCondition(
conditionItem,
firstInGroup = index == 0,
lastInGroup = index == conditions.size - 1
)
}
data.addAll(conditionDataModel)
.let { addAll(it) }
}

return data
}

private val LocalDate.localizedString: String
Expand All @@ -176,21 +201,6 @@ class PatientDetailsViewModel(

private fun getString(resId: Int) = getApplication<Application>().resources.getString(resId)

private suspend fun getPatientRiskAssessment(): RiskAssessmentItem {
val riskAssessment =
fhirEngine
.search<RiskAssessment> { filter(RiskAssessment.SUBJECT, { value = "Patient/$patientId" }) }
.filter { it.hasOccurrence() }
.sortedByDescending { it.occurrenceDateTimeType.value }
.firstOrNull()
return RiskAssessmentItem(
getRiskAssessmentStatusColor(riskAssessment),
getRiskAssessmentStatus(riskAssessment),
getLastContactedDate(riskAssessment),
getPatientDetailsCardColor(riskAssessment)
)
}

private fun getRiskAssessmentStatusColor(riskAssessment: RiskAssessment?): Int {
riskAssessment?.let {
return when (it.prediction.first().qualitativeRisk.coding.first().code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.StringFilterModifier
import com.google.android.fhir.search.count
import com.google.android.fhir.search.search
Expand Down Expand Up @@ -82,7 +81,6 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi
}
)
}
filterCity(this)
}
}

Expand All @@ -99,12 +97,11 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi
}
)
}
filterCity(this)
sort(Patient.GIVEN, Order.ASCENDING)
count = 100
from = 0
}
.mapIndexed { index, fhirPatient -> fhirPatient.toPatientItem(index + 1) }
.mapIndexed { index, fhirPatient -> fhirPatient.resource.toPatientItem(index + 1) }
.let { patients.addAll(it) }

val risks = getRiskAssessments()
Expand All @@ -116,19 +113,15 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi
return patients
}

private fun filterCity(search: Search) {
search.filter(Patient.ADDRESS_CITY, { value = "NAIROBI" })
}

private suspend fun getRiskAssessments(): Map<String, RiskAssessment?> {
return fhirEngine
.search<RiskAssessment> {}
.groupBy { it.subject.reference }
.groupBy { it.resource.subject.reference }
.mapValues { entry ->
entry.value
.filter { it.hasOccurrence() }
.sortedByDescending { it.occurrenceDateTimeType.value }
.firstOrNull()
.filter { it.resource.hasOccurrence() }
.maxByOrNull { it.resource.occurrenceDateTimeType.value }
?.resource
}
}

Expand Down

0 comments on commit b7b8b98

Please sign in to comment.