diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataPullTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataPullTest.kt index 833130ac093..bcfb616cd7b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataPullTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataPullTest.kt @@ -3,7 +3,11 @@ package org.odk.collect.android.feature.formentry.dynamicpreload import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain -import org.odk.collect.android.support.rules.FormEntryActivityTestRule +import org.odk.collect.android.support.StubOpenRosaServer.EntityListItem +import org.odk.collect.android.support.StubOpenRosaServer.MediaFileItem +import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.FormEntryPage +import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain.chain /** @@ -12,16 +16,36 @@ import org.odk.collect.android.support.rules.TestRuleChain.chain */ class DynamicPreLoadedDataPullTest { - private val rule = FormEntryActivityTestRule() + private val rule = CollectTestRule(useDemoProject = false) + private val testDependencies = TestDependencies() @get:Rule - val copyFormChain: RuleChain = chain() - .around(rule) + val chain: RuleChain = chain(testDependencies).around(rule) @Test fun canUsePullDataFunctionToPullDataFromCSV() { - rule.setUpProjectAndCopyForm("pull_data.xml", listOf("fruits.csv")) - .fillNewForm("pull_data.xml", "pull_data") + testDependencies.server.addForm("pull_data.xml", listOf(MediaFileItem("fruits.csv"))) + + rule.withMatchExactlyProject(testDependencies.server.url) + .startBlankForm("pull_data") .assertText("The fruit Mango is pulled csv data.") } + + @Test + fun canUsePullDataFunctionToPullDataFromLocalEntities() { + testDependencies.server.addForm("one-question-entity-registration.xml") + testDependencies.server.addForm( + "entity-update-pulldata.xml", + listOf(EntityListItem("people.csv")) + ) + + rule.withMatchExactlyProject(testDependencies.server.url) + .startBlankForm("One Question Entity Registration") + .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Logan Roy")) + + .startBlankForm("Entity Update Pull Data") + .clickOnText("Logan Roy") + .swipeToNextQuestion("Name") + .assertText("Logan Roy") + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt index 93d0fa328b2..c2fe398870d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt @@ -8,6 +8,7 @@ import org.odk.collect.android.dynamicpreload.ExternalDataManagerImpl import org.odk.collect.android.dynamicpreload.handler.ExternalDataHandlerPull import org.odk.collect.android.tasks.FormLoaderTask.FormEntryControllerFactory import org.odk.collect.entities.javarosa.filter.LocalEntitiesFilterStrategy +import org.odk.collect.entities.javarosa.filter.PullDataFunctionHandler import org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor import org.odk.collect.entities.storage.EntitiesRepository import org.odk.collect.settings.keys.ProjectKeys @@ -25,7 +26,8 @@ class CollectFormEntryControllerFactory( } return FormEntryController(FormEntryModel(formDef)).also { - it.addFunctionHandler(ExternalDataHandlerPull(externalDataManager)) + val externalDataHandlerPull = ExternalDataHandlerPull(externalDataManager) + it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository, externalDataHandlerPull)) it.addPostProcessor(EntityFormFinalizationProcessor()) if (settings.getBoolean(ProjectKeys.KEY_LOCAL_ENTITIES)) { diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategy.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategy.kt index c2ab628864d..55b39698559 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategy.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategy.kt @@ -22,7 +22,7 @@ import java.util.function.Supplier class LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) : FilterStrategy { - private val dataAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + private val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) override fun filter( sourceInstance: DataInstance<*>, @@ -32,7 +32,7 @@ class LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) : evaluationContext: EvaluationContext, next: Supplier> ): List { - if (sourceInstance.instanceId == null || !dataAdapter.supportsInstance(sourceInstance.instanceId)) { + if (sourceInstance.instanceId == null || !instanceAdapter.supportsInstance(sourceInstance.instanceId)) { return next.get() } @@ -43,7 +43,7 @@ class LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) : val child = candidate.nodeSide.steps[0].name.name val value = candidate.evalContextSide(sourceInstance, evaluationContext) - val results = dataAdapter.queryEq( + val results = instanceAdapter.queryEq( sourceInstance.instanceId, child, value as String diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandler.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandler.kt new file mode 100644 index 00000000000..5c31a1d427b --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandler.kt @@ -0,0 +1,49 @@ +package org.odk.collect.entities.javarosa.filter + +import org.javarosa.core.model.condition.EvaluationContext +import org.javarosa.core.model.condition.IFunctionHandler +import org.javarosa.xpath.expr.XPathFuncExpr +import org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceAdapter +import org.odk.collect.entities.storage.EntitiesRepository + +class PullDataFunctionHandler( + entitiesRepository: EntitiesRepository, + private val fallback: IFunctionHandler? = null +) : IFunctionHandler { + + private val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + + override fun getName(): String { + return NAME + } + + override fun getPrototypes(): List>> { + return emptyList() + } + + override fun rawArgs(): Boolean { + return true + } + + override fun realTime(): Boolean { + return false + } + + override fun eval(args: Array, ec: EvaluationContext): Any { + val instanceId = XPathFuncExpr.toString(args[0]) + val child = XPathFuncExpr.toString(args[1]) + val filterChild = XPathFuncExpr.toString(args[2]) + val filterValue = XPathFuncExpr.toString(args[3]) + + return if (instanceAdapter.supportsInstance(instanceId)) { + instanceAdapter.queryEq(instanceId, filterChild, filterValue)!!.firstOrNull() + ?.getFirstChild(child)?.value?.value ?: "" + } else { + fallback?.eval(args, ec) ?: "" + } + } + + companion object { + private const val NAME = "pulldata" + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceAdapter.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceAdapter.kt index 38870636b9c..d6a18c993b4 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceAdapter.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceAdapter.kt @@ -23,7 +23,7 @@ class LocalEntitiesInstanceAdapter(private val entitiesRepository: EntitiesRepos 0.until(count).map { if (it == 0) { - convertToElement(first, true) + convertToElement(first) } else { TreeElement("item", it, true) } @@ -33,62 +33,82 @@ class LocalEntitiesInstanceAdapter(private val entitiesRepository: EntitiesRepos } } else { entitiesRepository.getEntities(instanceId).map { entity -> - convertToElement(entity, false) + convertToElement(entity) } } } fun queryEq(instanceId: String, child: String, value: String): List? { return when { - child == "name" -> { + child == EntityItemElement.ID -> { val entity = entitiesRepository.getById( instanceId, value ) if (entity != null) { - listOf(convertToElement(entity, false)) + listOf(convertToElement(entity)) } else { emptyList() } } - !listOf(EntityItemElement.LABEL, EntityItemElement.VERSION).contains(child) -> { + child == EntityItemElement.LABEL -> { + filterAndConvertEntities(instanceId) { it.label == value } + } + + child == EntityItemElement.VERSION -> { + filterAndConvertEntities(instanceId) { it.version == value.toInt() } + } + + child == EntityItemElement.TRUNK_VERSION -> { + filterAndConvertEntities(instanceId) { it.trunkVersion == value.toInt() } + } + + child == EntityItemElement.BRANCH_ID -> { + filterAndConvertEntities(instanceId) { it.branchId == value } + } + + else -> { val entities = entitiesRepository.getAllByProperty( instanceId, child, value ) - entities.map { convertToElement(it, false) } + entities.map { convertToElement(it) } } - - else -> null } } - private fun convertToElement(entity: Entity.Saved, partial: Boolean): TreeElement { + private fun filterAndConvertEntities( + list: String, + filter: (Entity.Saved) -> Boolean + ): List { + val entities = entitiesRepository.getEntities(list) + return entities.filter(filter).map { convertToElement(it) } + } + + private fun convertToElement(entity: Entity.Saved): TreeElement { val name = TreeElement(EntityItemElement.ID) val label = TreeElement(EntityItemElement.LABEL) val version = TreeElement(EntityItemElement.VERSION) val trunkVersion = TreeElement(EntityItemElement.TRUNK_VERSION) val branchId = TreeElement(EntityItemElement.BRANCH_ID) - if (!partial) { - name.value = StringData(entity.id) - version.value = StringData(entity.version.toString()) - branchId.value = StringData(entity.branchId) + name.value = StringData(entity.id) + version.value = StringData(entity.version.toString()) + branchId.value = StringData(entity.branchId) - if (entity.label != null) { - label.value = StringData(entity.label) - } + if (entity.label != null) { + label.value = StringData(entity.label) + } - if (entity.trunkVersion != null) { - trunkVersion.value = StringData(entity.trunkVersion.toString()) - } + if (entity.trunkVersion != null) { + trunkVersion.value = StringData(entity.trunkVersion.toString()) } - val item = TreeElement("item", entity.index, partial) + val item = TreeElement("item", entity.index, false) item.addChild(name) item.addChild(label) item.addChild(version) @@ -97,11 +117,7 @@ class LocalEntitiesInstanceAdapter(private val entitiesRepository: EntitiesRepos entity.properties.forEach { property -> val propertyElement = TreeElement(property.first) - - if (!partial) { - propertyElement.value = StringData(property.second) - } - + propertyElement.value = StringData(property.second) item.addChild(propertyElement) } diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesInstanceProviderTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesInstanceProviderTest.kt index decfd1ccc3f..8784ec2d21b 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesInstanceProviderTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesInstanceProviderTest.kt @@ -111,7 +111,7 @@ class LocalEntitiesInstanceProviderTest { } @Test - fun `partial parse returns elements without values for first item and just item for others`() { + fun `partial parse returns the full first item and just item for others`() { val entity = arrayOf( Entity.New( "1", @@ -133,11 +133,9 @@ class LocalEntitiesInstanceProviderTest { assertThat(instance.numChildren, equalTo(2)) val item1 = instance.getChildAt(0)!! - assertThat(item1.isPartial, equalTo(true)) + assertThat(item1.isPartial, equalTo(false)) assertThat(item1.numChildren, equalTo(6)) - 0.until(item1.numChildren).forEach { - assertThat(item1.getChildAt(it).value?.value, equalTo(null)) - } + assertThat(item1.getFirstChild("name")!!.value!!.value, equalTo("1")) val item2 = instance.getChildAt(1)!! assertThat(item2.isPartial, equalTo(true)) diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandlerTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandlerTest.kt new file mode 100644 index 00000000000..c9d1dc668d3 --- /dev/null +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandlerTest.kt @@ -0,0 +1,104 @@ +package org.odk.collect.entities.javarosa.filter + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.javarosa.core.model.data.StringData +import org.javarosa.form.api.FormEntryController +import org.javarosa.form.api.FormEntryModel +import org.javarosa.test.BindBuilderXFormsElement.bind +import org.javarosa.test.Scenario +import org.javarosa.test.XFormsElement.body +import org.javarosa.test.XFormsElement.head +import org.javarosa.test.XFormsElement.html +import org.javarosa.test.XFormsElement.input +import org.javarosa.test.XFormsElement.mainInstance +import org.javarosa.test.XFormsElement.model +import org.javarosa.test.XFormsElement.t +import org.javarosa.test.XFormsElement.title +import org.junit.Test +import org.odk.collect.entities.storage.Entity +import org.odk.collect.entities.storage.InMemEntitiesRepository + +class PullDataFunctionHandlerTest { + + @Test + fun `returns empty string when there are no matching results`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.addList("things") + + val scenario = Scenario.init( + "Pull data form", + html( + head( + title("Pull data form"), + model( + mainInstance( + t( + "data id=\"pull-data-form\"", + t("question"), + t("calculate") + ) + ), + bind("/data/question").type("string"), + bind("/data/calculate").type("string") + .calculate("pulldata('things', 'label', 'name', 'blah')") + ) + ), + body( + input("/data/question"), + input("/data/calculate") + ) + ) + ) { formDef -> + FormEntryController(FormEntryModel(formDef)).also { + it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository)) + } + } + + assertThat(scenario.answerOf("/data/calculate"), equalTo(null)) + } + + @Test + fun `returns first match when there are multiple`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.save( + "things", + Entity.New("one", "One", properties = listOf(Pair("property", "value"))) + ) + entitiesRepository.save( + "things", + Entity.New("two", "Two", properties = listOf(Pair("property", "value"))) + ) + + val scenario = Scenario.init( + "Pull data form", + html( + head( + title("Pull data form"), + model( + mainInstance( + t( + "data id=\"pull-data-form\"", + t("question"), + t("calculate") + ) + ), + bind("/data/question").type("string"), + bind("/data/calculate").type("string") + .calculate("pulldata('things', 'label', 'property', 'value')") + ) + ), + body( + input("/data/question"), + input("/data/calculate") + ) + ) + ) { formDef -> + FormEntryController(FormEntryModel(formDef)).also { + it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository)) + } + } + + assertThat(scenario.answerOf("/data/calculate").value, equalTo("One")) + } +} diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/instance/LocalEntitiesInstanceAdapterTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/instance/LocalEntitiesInstanceAdapterTest.kt new file mode 100644 index 00000000000..4cf9db6e754 --- /dev/null +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/instance/LocalEntitiesInstanceAdapterTest.kt @@ -0,0 +1,60 @@ +package org.odk.collect.entities.javarosa.instance + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceAdapter +import org.odk.collect.entities.javarosa.parse.EntityItemElement +import org.odk.collect.entities.storage.Entity +import org.odk.collect.entities.storage.InMemEntitiesRepository + +class LocalEntitiesInstanceAdapterTest { + + @Test + fun `#queryEq supports label`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.save("things", Entity.New("thing1", "Thing 1")) + entitiesRepository.save("things", Entity.New("thing2", "Thing 2")) + + val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + val results = instanceAdapter.queryEq("things", EntityItemElement.LABEL, "Thing 2") + assertThat(results!!.size, equalTo(1)) + assertThat(results.first().getFirstChild("name")!!.value!!.value, equalTo("thing2")) + } + + @Test + fun `#queryEq supports __version`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.save("things", Entity.New("thing1", "Thing 1", version = 1)) + entitiesRepository.save("things", Entity.New("thing2", "Thing 2", version = 2)) + + val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + val results = instanceAdapter.queryEq("things", EntityItemElement.VERSION, "2") + assertThat(results!!.size, equalTo(1)) + assertThat(results.first().getFirstChild("name")!!.value!!.value, equalTo("thing2")) + } + + @Test + fun `#queryEq supports __trunkVersion`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.save("things", Entity.New("thing1", "Thing 1", trunkVersion = 1)) + entitiesRepository.save("things", Entity.New("thing2", "Thing 2", trunkVersion = 2)) + + val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + val results = instanceAdapter.queryEq("things", EntityItemElement.TRUNK_VERSION, "2") + assertThat(results!!.size, equalTo(1)) + assertThat(results.first().getFirstChild("name")!!.value!!.value, equalTo("thing2")) + } + + @Test + fun `#queryEq supports __branchId`() { + val entitiesRepository = InMemEntitiesRepository() + entitiesRepository.save("things", Entity.New("thing1", "Thing 1", branchId = "branch1")) + entitiesRepository.save("things", Entity.New("thing2", "Thing 2", branchId = "branch2")) + + val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository) + val results = instanceAdapter.queryEq("things", EntityItemElement.BRANCH_ID, "branch2") + assertThat(results!!.size, equalTo(1)) + assertThat(results.first().getFirstChild("name")!!.value!!.value, equalTo("thing2")) + } +} diff --git a/test-forms/src/main/resources/forms/entity-update-pulldata.xml b/test-forms/src/main/resources/forms/entity-update-pulldata.xml new file mode 100644 index 00000000000..f309db53f42 --- /dev/null +++ b/test-forms/src/main/resources/forms/entity-update-pulldata.xml @@ -0,0 +1,49 @@ + + + + Entity Update Pull Data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +