From 59658c61743f23ebefd64f034781c262afe231d8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 09:49:43 +0200 Subject: [PATCH 1/8] Added a failing test --- .../feature/formentry/ExternalSelectsTest.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt index 35ab9601b82..b21eee5fb5f 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt @@ -56,4 +56,30 @@ 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") + .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") + } } From be112eb8db680ef92b3646b43257c3f1d69fd781 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 09:51:44 +0200 Subject: [PATCH 2/8] Update question answer after loading external choices --- .../dynamicpreload/ExternalDataUtil.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java b/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java index fddf9501431..4e4bc48a292 100644 --- a/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java +++ b/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java @@ -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; @@ -210,6 +212,7 @@ public static ArrayList populateExternalChoices(FormEntryPrompt fo } } } + updateQuestionAnswer(formEntryPrompt, returnedChoices); return returnedChoices; } catch (Exception e) { String fileName = String.valueOf(xpathfuncexpr.args[0].eval(null, null)); @@ -228,6 +231,23 @@ public static ArrayList populateExternalChoices(FormEntryPrompt fo } } + private static void updateQuestionAnswer(FormEntryPrompt formEntryPrompt, List returnedChoices) { + IAnswerData value = formEntryPrompt.getAnswerValue(); + if (value == null) { + return; + } + + Selection selection = (Selection) value.getValue(); + if (selection.index != -1) { + return; + } + + returnedChoices.stream() + .filter(choice -> selection.getValue().equals(choice.getValue())) + .findFirst() + .ifPresent(selection::attachChoice); + } + /** * We could simple return new String(displayColumns + "," + valueColumn) but we want to handle * the cases From 5e0e80c3cc3f9b9b3f827a0f4ea2338aee574383 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 09:52:47 +0200 Subject: [PATCH 3/8] Call #updateQuestions after building them in ODKView --- .../main/java/org/odk/collect/android/formentry/ODKView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java index 48f520738d2..7ba6776e141 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java @@ -161,7 +161,6 @@ public ODKView( LifecycleOwner viewLifecycle ) { super(context); - updateQuestions(questionPrompts); this.viewLifecycle = viewLifecycle; this.audioPlayer = audioPlayer; @@ -209,6 +208,7 @@ public ODKView( setupAudioErrors(); autoplayIfNeeded(advancingPage); + updateQuestions(questionPrompts); } private void setupAudioErrors() { From ab8ca7367364e269de56306d90aad4acf57a2f03 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 09:57:58 +0200 Subject: [PATCH 4/8] If translatable answer for select_one can't be determined - return the value --- .../android/formhierarchy/QuestionAnswerProcessor.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt index 3604bda99a7..3a46816d2c7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt @@ -25,7 +25,12 @@ object QuestionAnswerProcessor { return "" } - if (!fep.answerText.isNullOrBlank() && + val answerText = try { + fep.answerText + } catch (e: Exception) { + fep.answerValue?.displayText + } + if (!answerText.isNullOrBlank() && Appearances.isMasked(fep) && fep.controlType == Constants.CONTROL_INPUT && fep.dataType == Constants.DATATYPE_TEXT @@ -72,7 +77,7 @@ object QuestionAnswerProcessor { if (data != null && appearance != null && appearance.contains(Appearances.THOUSANDS_SEP)) { return try { - val answerAsDecimal = BigDecimal(fep.answerText) + val answerAsDecimal = BigDecimal(answerText) val df = DecimalFormat() df.groupingSize = 3 @@ -101,6 +106,6 @@ object QuestionAnswerProcessor { } return ItemsetDao(ItemsetDbAdapter()).getItemLabel(fep.answerValue!!.displayText, formController.getMediaFolder()!!.absolutePath, language) } - return fep.answerText ?: "" + return answerText ?: "" } } From 662be56b32d74580e72db1976af57a9d3f86760b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 14:29:01 +0200 Subject: [PATCH 5/8] Added missing form --- .../forms/search-with-last-saved.xml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test-forms/src/main/resources/forms/search-with-last-saved.xml diff --git a/test-forms/src/main/resources/forms/search-with-last-saved.xml b/test-forms/src/main/resources/forms/search-with-last-saved.xml new file mode 100644 index 00000000000..4fab2fed89c --- /dev/null +++ b/test-forms/src/main/resources/forms/search-with-last-saved.xml @@ -0,0 +1,55 @@ + + + + Search with last-saved + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + name_key + + + + + + + + name_key + + + + + + + + \ No newline at end of file From 8ca4cea2d1b32d61543deec26b4767d681d526e7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 14:29:41 +0200 Subject: [PATCH 6/8] Revert "If translatable answer for select_one can't be determined - return the value" This reverts commit 7710bc8e0d137504dbb3e51d074f0cbc5cf73eca. --- .../android/formhierarchy/QuestionAnswerProcessor.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt index 3a46816d2c7..3604bda99a7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt @@ -25,12 +25,7 @@ object QuestionAnswerProcessor { return "" } - val answerText = try { - fep.answerText - } catch (e: Exception) { - fep.answerValue?.displayText - } - if (!answerText.isNullOrBlank() && + if (!fep.answerText.isNullOrBlank() && Appearances.isMasked(fep) && fep.controlType == Constants.CONTROL_INPUT && fep.dataType == Constants.DATATYPE_TEXT @@ -77,7 +72,7 @@ object QuestionAnswerProcessor { if (data != null && appearance != null && appearance.contains(Appearances.THOUSANDS_SEP)) { return try { - val answerAsDecimal = BigDecimal(answerText) + val answerAsDecimal = BigDecimal(fep.answerText) val df = DecimalFormat() df.groupingSize = 3 @@ -106,6 +101,6 @@ object QuestionAnswerProcessor { } return ItemsetDao(ItemsetDbAdapter()).getItemLabel(fep.answerValue!!.displayText, formController.getMediaFolder()!!.absolutePath, language) } - return answerText ?: "" + return fep.answerText ?: "" } } From 401ceebdb97c1d99d2b602e1d4b4fcf2e76369c0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 10 Jul 2025 14:38:35 +0200 Subject: [PATCH 7/8] Updated test to ignore the bug with building answers for the hierarchy --- .../collect/android/feature/formentry/ExternalSelectsTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt index b21eee5fb5f..4d8e59c04b0 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt @@ -71,9 +71,10 @@ class ExternalSelectsTest { // 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") + .assertHierarchyItem(1, "Select fruit 2", "Oranges") // Change an answer in a field-list and verify no errors occur .clickOnQuestion("Select fruit 2") From 80c1a4e5ae1b7e4c8b7994998174a5a68d08481b Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Wed, 19 Nov 2025 21:56:28 +0100 Subject: [PATCH 8/8] Resolve selected choice during main loop to avoid second pass --- .../dynamicpreload/ExternalDataUtil.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java b/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java index 4e4bc48a292..84740cf97bb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java +++ b/collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java @@ -168,12 +168,18 @@ public static XPathFuncExpr getSearchXPathExpression(String appearance) { public static ArrayList 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 selectChoices = formEntryPrompt.getSelectChoices(); ArrayList 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); @@ -203,6 +209,7 @@ public static ArrayList populateExternalChoices(FormEntryPrompt fo @SuppressWarnings("unchecked") List dynamicChoices = (ArrayList) eval; for (SelectChoice dynamicChoice : dynamicChoices) { + attachChoiceToSelectionIfMatch(selection, dynamicChoice); returnedChoices.add(dynamicChoice); } } else { @@ -212,7 +219,6 @@ public static ArrayList populateExternalChoices(FormEntryPrompt fo } } } - updateQuestionAnswer(formEntryPrompt, returnedChoices); return returnedChoices; } catch (Exception e) { String fileName = String.valueOf(xpathfuncexpr.args[0].eval(null, null)); @@ -231,21 +237,14 @@ public static ArrayList populateExternalChoices(FormEntryPrompt fo } } - private static void updateQuestionAnswer(FormEntryPrompt formEntryPrompt, List returnedChoices) { - IAnswerData value = formEntryPrompt.getAnswerValue(); - if (value == null) { + private static void attachChoiceToSelectionIfMatch(Selection selection, SelectChoice selectChoice) { + if (selection == null || selection.index != -1) { return; } - Selection selection = (Selection) value.getValue(); - if (selection.index != -1) { - return; + if (selection.getValue().equals(selectChoice.getValue())) { + selection.attachChoice(selectChoice); } - - returnedChoices.stream() - .filter(choice -> selection.getValue().equals(choice.getValue())) - .findFirst() - .ifPresent(selection::attachChoice); } /**