Skip to content

Commit

Permalink
Merge branch 'main' into add-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
pld authored Sep 20, 2024
2 parents 644dccf + 8179c81 commit 42fb650
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,5 @@ enum class LinkIdType : Parcelable {
READ_ONLY,
BARCODE,
LOCATION,
IDENTIFIER,
PREPOPULATION_EXCLUSION,
}
2 changes: 1 addition & 1 deletion android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ constructor(
): QuestionnaireResponse? {
val searchQuery =
createQuestionnaireResponseSearchQuery(questionnaireId, subjectId, subjectType)
return defaultRepository.search<QuestionnaireResponse>(searchQuery).firstOrNull()
return defaultRepository.search<QuestionnaireResponse>(searchQuery).maxByOrNull {
it.meta.lastUpdated
}
}

/**
Expand All @@ -77,8 +79,6 @@ constructor(
QuestionnaireResponse.QUESTIONNAIRE,
{ value = "${ResourceType.Questionnaire}/$questionnaireId" },
)
count = 1
from = 0
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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
Expand Down Expand Up @@ -1118,14 +1119,49 @@ constructor(
null
}

// Exclude the configured fields from QR
if (questionnaireResponse != null) {
val exclusionLinkIdsMap: Map<String, Boolean> =
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<QuestionnaireResponseItemComponent>,
exclusionMap: Map<String, Boolean>,
): MutableList<QuestionnaireResponseItemComponent> {
val stack = LinkedList<MutableList<QuestionnaireResponseItemComponent>>()
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<QuestionnaireResponseItemComponent>.removeUnAnsweredItems():
List<QuestionnaireResponseItemComponent> {
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()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<QuestionnaireResponse>(any<Search>()) } returns
questionnaireResponses
val result =
viewModel.retrieveQuestionnaireResponse(questionnaire.id, patient.id, ResourceType.Patient)

assertEquals(latestQuestionnaireResponse.id, result!!.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<QuestionnaireResponse>(any<Search>()) } 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 =
Expand Down Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions docs/engineering/app/configuring/forms/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
```
Loading

0 comments on commit 42fb650

Please sign in to comment.