Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,31 @@ class ExternalSelectsTest {
.assertText("File: $formsDirPath/search_and_select-media/nombre.csv is missing.")
.assertText("File: $formsDirPath/search_and_select-media/nombre2.csv is missing.")
}

@Test // https://github.com/getodk/collect/issues/6801
fun searchFunctionWorksWellWithLastSaved() {
rule.startAtMainMenu()
// Fill out and finalize the first form
.copyForm("search-with-last-saved.xml", listOf("fruits.csv"))
.startBlankForm("Search with last-saved")
.clickOnText("Mango")
.swipeToNextQuestion("Select fruit 2")
.clickOnText("Oranges")
.swipeToEndScreen()
.clickFinalize()

// Start a new form to verify that answers from the previous form are retained
.startBlankForm("Search with last-saved")
.swipeToNextQuestion("Select fruit 2")
.clickGoToArrow()
.assertHierarchyItem(0, "Select fruit 1", "Mango")
.assertHierarchyItem(1, "Select fruit 2", "Oranges")

// Change an answer in a field-list and verify no errors occur
.clickOnQuestion("Select fruit 2")
.clickOnText("Strawberries")
.clickGoToArrow()
.assertHierarchyItem(0, "Select fruit 1", "Mango")
.assertHierarchyItem(1, "Select fruit 2", "Strawberries")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import org.javarosa.core.model.SelectChoice;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.data.helper.Selection;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.form.api.FormEntryPrompt;
Expand Down Expand Up @@ -166,12 +168,18 @@ public static XPathFuncExpr getSearchXPathExpression(String appearance) {
public static ArrayList<SelectChoice> populateExternalChoices(FormEntryPrompt formEntryPrompt,
XPathFuncExpr xpathfuncexpr, FormController formController) throws FileNotFoundException {
try {
IAnswerData selectedValue = formEntryPrompt.getAnswerValue();
Selection selection = null;
if (selectedValue != null) {
selection = (Selection) selectedValue.getValue();
}
List<SelectChoice> selectChoices = formEntryPrompt.getSelectChoices();
ArrayList<SelectChoice> returnedChoices = new ArrayList<>();
for (SelectChoice selectChoice : selectChoices) {
String value = selectChoice.getValue();
if (isAnInteger(value)) {
// treat this as a static choice
attachChoiceToSelectionIfMatch(selection, selectChoice);
returnedChoices.add(selectChoice);
} else {
String displayColumns = formEntryPrompt.getSelectChoiceText(selectChoice);
Expand Down Expand Up @@ -201,6 +209,7 @@ public static ArrayList<SelectChoice> populateExternalChoices(FormEntryPrompt fo
@SuppressWarnings("unchecked")
List<SelectChoice> dynamicChoices = (ArrayList<SelectChoice>) eval;
for (SelectChoice dynamicChoice : dynamicChoices) {
attachChoiceToSelectionIfMatch(selection, dynamicChoice);
returnedChoices.add(dynamicChoice);
}
} else {
Expand Down Expand Up @@ -228,6 +237,16 @@ public static ArrayList<SelectChoice> populateExternalChoices(FormEntryPrompt fo
}
}

private static void attachChoiceToSelectionIfMatch(Selection selection, SelectChoice selectChoice) {
if (selection == null || selection.index != -1) {
return;
}

if (selection.getValue().equals(selectChoice.getValue())) {
selection.attachChoice(selectChoice);
}
}

/**
* We could simple return new String(displayColumns + "," + valueColumn) but we want to handle
* the cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ public ODKView(
LifecycleOwner viewLifecycle
) {
super(context);
updateQuestions(questionPrompts);

this.viewLifecycle = viewLifecycle;
this.audioPlayer = audioPlayer;
Expand Down Expand Up @@ -209,6 +208,7 @@ public ODKView(

setupAudioErrors();
autoplayIfNeeded(advancingPage);
updateQuestions(questionPrompts);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the steps performed by #updateQuestions is reading the user-visible answers in order to compare the entire content and determine whether a question has entered a new state that should be displayed: https://github.com/getodk/collect/blob/master/collect_app/src/main/java/org/odk/collect/android/logic/ImmutableDisplayableQuestion.java#L84.

For select-type questions, the underlying data is represented by Selection, which contains the user-visible answer, the index, and an XML value. If we attempt to perform the update at the beginning of this block, answers from external choices (those using the search function) are not yet available, although we already have access to the XML value. In such cases, trying to retrieve the user-visible answer results in an error. Therefore, we must defer this operation until all widgets are built and all choices have been fully loaded.

}

private void setupAudioErrors() {
Expand Down
55 changes: 55 additions & 0 deletions test-forms/src/main/resources/forms/search-with-last-saved.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<h:html
xmlns="http://www.w3.org/2002/xforms"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms"
xmlns:odk="http://www.opendatakit.org/xforms">
<h:head>
<h:title>Search with last-saved</h:title>
<model odk:xforms-version="1.0.0">
<instance>
<data id="search-with-last-saved">
<fruit/>
<group>
<fruit2/>
<details/>
</group>
<meta>
<instanceID/>
</meta>
</data>
</instance>
<instance id="__last-saved" src="jr://instance/last-saved"/>
<bind nodeset="/data/fruit" type="string"/>
<setvalue ref="/data/fruit" value=" instance('__last-saved')/data/fruit " event="odk-instance-first-load"/>
<bind nodeset="/data/group/fruit2" type="string"/>
<setvalue ref="/data/group/fruit2" value=" instance('__last-saved')/data/group/fruit2 " event="odk-instance-first-load"/>
<bind nodeset="/data/group/details" type="string"/>
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
</model>
</h:head>
<h:body>
<select1 ref="/data/fruit" appearance="search('fruits')">
<label>Select fruit 1</label>
<item>
<label>name</label>
<value>name_key</value>
</item>
</select1>
<group appearance="field-list" ref="/data/group">
<select1 ref="/data/group/fruit2" appearance="search('fruits')">
<label>Select fruit 2</label>
<item>
<label>name</label>
<value>name_key</value>
</item>
</select1>
<input ref="/data/group/details">
<label>Fruit details</label>
</input>
</group>
</h:body>
</h:html>