From 7d483f14153b5bb9b26e20b7c7fdc3e4f2093bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 25 Mar 2021 16:28:44 -0700 Subject: [PATCH] Bind instance answer value(s) to SelectChoice Clients need this to get label text for the selected answer(s). --- .../javarosa/core/model/ItemsetBinding.java | 52 ++++---- .../javarosa/form/api/FormEntryPrompt.java | 22 ++-- .../core/model/SelectOneChoiceFilterTest.java | 6 +- .../java/org/javarosa/core/test/Scenario.java | 4 + .../form/api/FormEntryPromptTest.java | 111 ++++++++++++++++++ 5 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 src/test/java/org/javarosa/form/api/FormEntryPromptTest.java diff --git a/src/main/java/org/javarosa/core/model/ItemsetBinding.java b/src/main/java/org/javarosa/core/model/ItemsetBinding.java index f778243eb..4222ff804 100644 --- a/src/main/java/org/javarosa/core/model/ItemsetBinding.java +++ b/src/main/java/org/javarosa/core/model/ItemsetBinding.java @@ -17,6 +17,7 @@ import org.javarosa.core.model.condition.IConditionExpr; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.MultipleItemsData; +import org.javarosa.core.model.data.SelectOneData; import org.javarosa.core.model.data.StringData; import org.javarosa.core.model.data.helper.Selection; import org.javarosa.core.model.instance.DataInstance; @@ -114,14 +115,14 @@ public List getChoices(FormDef formDef, TreeReference curQRef) { throw new XPathException("Could not find references depended on by" + nodesetRef.getInstanceName()); } - Map currentAnswersInNewChoices = initializeCurrentAnswerMap(formDef, curQRef); + Map currentAnswersInNewChoices = initializeCurrentAnswerMap(formDef, curQRef); List choices = new ArrayList<>(); for (int i = 0; i < filteredItemReferences.size(); i++) { SelectChoice choice = getChoiceForTreeReference(formDef, formInstance, i, filteredItemReferences.get(i)); choices.add(choice); if (currentAnswersInNewChoices != null && currentAnswersInNewChoices.containsKey(choice.getValue())) { - currentAnswersInNewChoices.put(choice.getValue(), true); + currentAnswersInNewChoices.put(choice.getValue(), choice); } } @@ -200,53 +201,60 @@ private SelectChoice getChoiceForTreeReference(FormDef formDef, DataInstance for return choice; } - // Build a map with keys for each value in the current answer. This will allow us to remove answers that are no - // longer available for selection because of an updated filter. - private Map initializeCurrentAnswerMap(FormDef formDef, TreeReference curQRef) { - Map currentAnswersInNewChoices = null; + /** + * Build a map with keys for each value in the current answer. This will allow us to remove answers that are no + * longer available for selection because of an updated filter and to bind selection(s) in the IAnswerData to Selection + * objects. The latter is necessary to get label text for the answers. + */ + private Map initializeCurrentAnswerMap(FormDef formDef, TreeReference curQRef) { + Map currentAnswersInNewChoices = null; IAnswerData rawValue = formDef.getMainInstance().resolveReference(curQRef).getValue(); if (rawValue != null) { currentAnswersInNewChoices = new HashMap<>(); if (rawValue instanceof MultipleItemsData) { for (Selection selection : (List) rawValue.getValue()) { - currentAnswersInNewChoices.put(selection.choice != null ? selection.choice.getValue() : selection.xmlValue, false); + currentAnswersInNewChoices.put(selection.choice != null ? selection.choice.getValue() : selection.xmlValue, null); } } else { - currentAnswersInNewChoices.put(rawValue.getDisplayText(), false); + currentAnswersInNewChoices.put(rawValue.getDisplayText(), null); } } return currentAnswersInNewChoices; } - private void updateQuestionAnswerInModel(FormDef formDef, TreeReference curQRef, Map currentAnswersInNewChoices) { - IAnswerData rawValue = formDef.getMainInstance().resolveReference(curQRef).getValue(); + private void updateQuestionAnswerInModel(FormDef formDef, TreeReference curQRef, Map currentAnswersInNewChoices) { + IAnswerData originalValue = formDef.getMainInstance().resolveReference(curQRef).getValue(); - if (currentAnswersInNewChoices != null && currentAnswersInNewChoices.containsValue(false)) { - IAnswerData filteredAnswer; - if (rawValue instanceof MultipleItemsData) { - filteredAnswer = getFilteredSelections((MultipleItemsData) rawValue, currentAnswersInNewChoices); + if (currentAnswersInNewChoices != null) { + IAnswerData boundAndFilteredValue; + if (originalValue instanceof MultipleItemsData) { + boundAndFilteredValue = getFilteredAndBoundSelections((MultipleItemsData) originalValue, currentAnswersInNewChoices); + } else if (currentAnswersInNewChoices.containsValue(null)) { + boundAndFilteredValue = new StringData(""); } else { - filteredAnswer = new StringData(""); + SelectChoice selectChoice = currentAnswersInNewChoices.get(originalValue.getDisplayText()); + boundAndFilteredValue = new SelectOneData(selectChoice.selection()); } - formDef.getMainInstance().resolveReference(curQRef).setAnswer(filteredAnswer); + formDef.getMainInstance().resolveReference(curQRef).setAnswer(boundAndFilteredValue); } } /** * @param selections an answer to a multiple selection question - * @param shouldKeepSelection maps each value that could be in @{code selections} to a boolean representing whether - * or not it should be kept - * @return a copy of {@code selections} without the values that were mapped to false in {@code shouldKeepSelection} + * @param selectChoicesToKeep maps each value that could be in @{code selections} to a SelectChoice if it should be bound + * or null if it should be removed. + * @return a copy of {@code selections} without the values that were mapped to false in {@code selectChoicesToKeep} and + * with all selections bound. */ - private static MultipleItemsData getFilteredSelections(MultipleItemsData selections, Map shouldKeepSelection) { + private static MultipleItemsData getFilteredAndBoundSelections(MultipleItemsData selections, Map selectChoicesToKeep) { List newSelections = new ArrayList<>(); for (Selection oldSelection : (List) selections.getValue()) { String key = oldSelection.choice != null ? oldSelection.choice.getValue() : oldSelection.xmlValue; - if (shouldKeepSelection.get(key)) { - newSelections.add(oldSelection); + if (selectChoicesToKeep.get(key) != null) { + newSelections.add(selectChoicesToKeep.get(key).selection()); } } diff --git a/src/main/java/org/javarosa/form/api/FormEntryPrompt.java b/src/main/java/org/javarosa/form/api/FormEntryPrompt.java index d4e77a0d5..0ed2fae05 100644 --- a/src/main/java/org/javarosa/form/api/FormEntryPrompt.java +++ b/src/main/java/org/javarosa/form/api/FormEntryPrompt.java @@ -40,8 +40,6 @@ import org.javarosa.core.util.UnregisteredLocaleException; import org.javarosa.formmanager.view.IQuestionWidget; - - /** * This class gives you all the information you need to display a question when * your current FormIndex references a QuestionEvent. @@ -289,27 +287,25 @@ public String getHelpText() { } - - /** - * Attempts to return the specified item text from a STATIC select or select1. Does NOT - * support dynamic selects ({@see ItemsetBinding}). + * Attempts to return the specified item text from a select or select1. + * * Will check for text in the following order:
* Localized Text (long form) -> Localized Text (no special form)
* If no textID is available, method will return this item's labelInnerText. + * * @param sel the selection (item), if null will throw a IllegalArgumentException * @return Question Text. null if no text for this element exists (after all fallbacks). - * @throws RunTimeException if this method is called on an element that is NOT a QuestionDef + * @throws RuntimeException if this method is called on an element that is NOT a QuestionDef * @throws IllegalArgumentException if Selection is null */ - public String getSelectItemText(Selection sel){ - //throw tantrum if this method is called when it shouldn't be or sel==null + public String getSelectItemText(Selection sel) { if (!(getFormElement() instanceof QuestionDef)) throw new RuntimeException("Can't retrieve question text for non-QuestionDef form elements!"); if (sel == null) throw new IllegalArgumentException("Cannot use null as an argument!"); - //Just in case the selection hasn't had a chance to be initialized yet. + // Just in case the selection hasn't had a chance to be initialized yet. if (sel.index == -1) { - sel.attachChoice(this.getQuestion()); + sel.attachChoice(getQuestion()); } //check for the null id case and return labelInnerText if it is so. @@ -325,7 +321,7 @@ public String getSelectItemText(Selection sel){ } /** - * @see getSelectItemText(Selection sel) + * @see #getSelectItemText(Selection sel) */ public String getSelectChoiceText(SelectChoice selection){ return getSelectItemText(selection.selection()); @@ -338,7 +334,7 @@ public String getSelectChoiceText(SelectChoice selection){ * @param sel - The Item whose text you're trying to retrieve. * @param form - Special text form of Item you're trying to retrieve. * @return Special Form Text. null if no text for this element exists (with the specified special form). - * @throws RunTimeException if this method is called on an element that is NOT a QuestionDef + * @throws RuntimeException if this method is called on an element that is NOT a QuestionDef * @throws IllegalArgumentException if sel == null */ public String getSpecialFormSelectItemText(Selection sel,String form){ diff --git a/src/test/java/org/javarosa/core/model/SelectOneChoiceFilterTest.java b/src/test/java/org/javarosa/core/model/SelectOneChoiceFilterTest.java index ed7713113..1ca665db5 100644 --- a/src/test/java/org/javarosa/core/model/SelectOneChoiceFilterTest.java +++ b/src/test/java/org/javarosa/core/model/SelectOneChoiceFilterTest.java @@ -142,12 +142,12 @@ public void changingValueAtLevel2_ShouldNotClearLevel3_IfChoiceStillAvailable() choice("baa"))); scenario.answer("/data/level3_contains", "aab"); scenario.answer("/data/level2_contains", "ab"); - assertThat(scenario.answerOf("/data/level3_contains"), is(stringAnswer("aab"))); + assertThat(scenario.answerOf("/data/level3_contains").getDisplayText(), is("aab")); - // Since populateDynamicChoices can change answers, verify it doesn't in this case + // Since recomputing the choice list can change answers, verify it doesn't in this case assertThat(scenario.choicesOf("/data/level3_contains"), containsInAnyOrder( choice("aab"), choice("bab"))); - assertThat(scenario.answerOf("/data/level3_contains"), is(stringAnswer("aab"))); + assertThat(scenario.answerOf("/data/level3_contains").getDisplayText(), is("aab")); } } diff --git a/src/test/java/org/javarosa/core/test/Scenario.java b/src/test/java/org/javarosa/core/test/Scenario.java index 70d30bf08..c41ac49b9 100644 --- a/src/test/java/org/javarosa/core/test/Scenario.java +++ b/src/test/java/org/javarosa/core/test/Scenario.java @@ -804,6 +804,10 @@ public QuestionDef getQuestionAtIndex() { return model.getQuestionPrompt().getQuestion(); } + public FormEntryPrompt getFormEntryPromptAtIndex() { + return model.getQuestionPrompt(); + } + // endregion // region Inspect the main instance diff --git a/src/test/java/org/javarosa/form/api/FormEntryPromptTest.java b/src/test/java/org/javarosa/form/api/FormEntryPromptTest.java new file mode 100644 index 000000000..a23f84d6b --- /dev/null +++ b/src/test/java/org/javarosa/form/api/FormEntryPromptTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 ODK + * + * 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.javarosa.form.api; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.javarosa.core.util.XFormsElement.body; +import static org.javarosa.core.util.XFormsElement.head; +import static org.javarosa.core.util.XFormsElement.html; +import static org.javarosa.core.util.XFormsElement.input; +import static org.javarosa.core.util.XFormsElement.instance; +import static org.javarosa.core.util.XFormsElement.item; +import static org.javarosa.core.util.XFormsElement.mainInstance; +import static org.javarosa.core.util.XFormsElement.model; +import static org.javarosa.core.util.XFormsElement.select1Dynamic; +import static org.javarosa.core.util.XFormsElement.t; +import static org.javarosa.core.util.XFormsElement.title; + +import java.io.IOException; +import org.javarosa.core.test.Scenario; +import org.junit.Test; + +public class FormEntryPromptTest { + @Test + public void getSelectItemText_onSelectionFromDynamicSelect_withoutTranslations_returnsLabelInnerText() throws IOException { + Scenario scenario = Scenario.init("Select", html( + head( + title("Select"), + model( + mainInstance( + t("data id='select'", + t("filter"), + t("select", "a"))), + + instance("choices", + item("a", "A"), + item("aa", "AA"), + item("b", "B"), + item("bb", "BB")))), + body( + input("/data/filter"), + select1Dynamic("/data/select", "instance('choices')/root/item[starts-with(value,/data/filter)]") + ))); + + scenario.next(); + scenario.answer("a"); + + scenario.next(); + FormEntryPrompt questionPrompt = scenario.getFormEntryPromptAtIndex(); + assertThat(questionPrompt.getAnswerText(), is("A")); + } + + @Test + public void getSelectItemText_onSelectionFromDynamicSelect_withTranslations_returnsCorrectTranslation() throws IOException { + Scenario scenario = Scenario.init("Multilingual dynamic select", html( + head( + title("Multilingual dynamic select"), + model( + t("itext", + t("translation lang='fr'", + t("text id='choices-0'", + t("value", "A (fr)")), + t("text id='choices-1'", + t("value", "B (fr)")), + t("text id='choices-2'", + t("value", "C (fr)")) + ), + t("translation lang='en'", + t("text id='choices-0'", + t("value", "A (en)")), + t("text id='choices-1'", + t("value", "B (en)")), + t("text id='choices-2'", + t("value", "C (en)")) + )), + mainInstance( + t("data id='multilingual-select'", + t("select", "b"))), + + instance("choices", + t("item", t("itextId", "choices-0"), t("name", "a")), + t("item", t("itextId", "choices-1"), t("name", "b")), + t("item", t("itextId", "choices-2"), t("name", "c"))))), + body( + select1Dynamic("/data/select", "instance('choices')/root/item", "name", "jr:itext(itextId)")) + )); + + scenario.setLanguage("en"); + + scenario.next(); + FormEntryPrompt questionPrompt = scenario.getFormEntryPromptAtIndex(); + assertThat(questionPrompt.getAnswerText(), is("B (en)")); + + scenario.setLanguage("fr"); + assertThat(questionPrompt.getAnswerText(), is("B (fr)")); + } +}