Skip to content

Commit

Permalink
Bind instance answer value(s) to SelectChoice
Browse files Browse the repository at this point in the history
Clients need this to get label text for the selected answer(s).
  • Loading branch information
lognaturel committed Mar 25, 2021
1 parent 18391aa commit 7d483f1
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 38 deletions.
52 changes: 30 additions & 22 deletions src/main/java/org/javarosa/core/model/ItemsetBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,14 +115,14 @@ public List<SelectChoice> getChoices(FormDef formDef, TreeReference curQRef) {
throw new XPathException("Could not find references depended on by" + nodesetRef.getInstanceName());
}

Map<String, Boolean> currentAnswersInNewChoices = initializeCurrentAnswerMap(formDef, curQRef);
Map<String, SelectChoice> currentAnswersInNewChoices = initializeCurrentAnswerMap(formDef, curQRef);

List<SelectChoice> 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);
}
}

Expand Down Expand Up @@ -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<String, Boolean> initializeCurrentAnswerMap(FormDef formDef, TreeReference curQRef) {
Map<String, Boolean> 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<String, SelectChoice> initializeCurrentAnswerMap(FormDef formDef, TreeReference curQRef) {
Map<String, SelectChoice> currentAnswersInNewChoices = null;
IAnswerData rawValue = formDef.getMainInstance().resolveReference(curQRef).getValue();
if (rawValue != null) {
currentAnswersInNewChoices = new HashMap<>();

if (rawValue instanceof MultipleItemsData) {
for (Selection selection : (List<Selection>) 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<String, Boolean> currentAnswersInNewChoices) {
IAnswerData rawValue = formDef.getMainInstance().resolveReference(curQRef).getValue();
private void updateQuestionAnswerInModel(FormDef formDef, TreeReference curQRef, Map<String, SelectChoice> 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<String, Boolean> shouldKeepSelection) {
private static MultipleItemsData getFilteredAndBoundSelections(MultipleItemsData selections, Map<String, SelectChoice> selectChoicesToKeep) {
List<Selection> newSelections = new ArrayList<>();
for (Selection oldSelection : (List<Selection>) 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());
}
}

Expand Down
22 changes: 9 additions & 13 deletions src/main/java/org/javarosa/form/api/FormEntryPrompt.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:<br/>
* Localized Text (long form) -> Localized Text (no special form) <br />
* If no textID is available, method will return this item's labelInnerText.
*
* @param sel the selection (item), if <code>null</code> will throw a IllegalArgumentException
* @return Question Text. <code>null</code> 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 <code>null</code>
*/
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.
Expand All @@ -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());
Expand All @@ -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. <code>null</code> 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 <code>sel == null</code>
*/
public String getSpecialFormSelectItemText(Selection sel,String form){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
4 changes: 4 additions & 0 deletions src/test/java/org/javarosa/core/test/Scenario.java
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,10 @@ public QuestionDef getQuestionAtIndex() {
return model.getQuestionPrompt().getQuestion();
}

public FormEntryPrompt getFormEntryPromptAtIndex() {
return model.getQuestionPrompt();
}

// endregion

// region Inspect the main instance
Expand Down
111 changes: 111 additions & 0 deletions src/test/java/org/javarosa/form/api/FormEntryPromptTest.java
Original file line number Diff line number Diff line change
@@ -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)"));
}
}

0 comments on commit 7d483f1

Please sign in to comment.