diff --git a/libs/commcare b/libs/commcare index 584456269..9242d2c21 160000 --- a/libs/commcare +++ b/libs/commcare @@ -1 +1 @@ -Subproject commit 58445626995a39e46d7c253942689a12a9832f77 +Subproject commit 9242d2c211f9103762c524ccfada4ad7272a71ed diff --git a/src/main/java/org/commcare/formplayer/api/json/JsonActionUtils.java b/src/main/java/org/commcare/formplayer/api/json/JsonActionUtils.java index 547cba30d..baa4a1c7e 100644 --- a/src/main/java/org/commcare/formplayer/api/json/JsonActionUtils.java +++ b/src/main/java/org/commcare/formplayer/api/json/JsonActionUtils.java @@ -37,14 +37,13 @@ public class JsonActionUtils { * * @param controller the FormEntryController under consideration * @param model the FormEntryModel under consideration - * @param formIndexString the form index of the repeat group to be deleted + * @param repeatIndexString the form index of the repeat group to be deleted * @return The JSON representation of the updated form tree */ public static JSONObject deleteRepeatToJson(FormEntryController controller, - FormEntryModel model, String repeatIndexString, String formIndexString) { - FormIndex formIndex = indexFromString(formIndexString, model.getForm()); - controller.jumpToIndex(formIndex); - controller.deleteRepeat(Integer.parseInt(repeatIndexString)); + FormEntryModel model, String repeatIndexString) { + FormIndex indexToDelete = indexFromString(repeatIndexString, model.getForm()); + controller.deleteRepeat(indexToDelete); return getCurrentJson(controller, model); } diff --git a/src/main/java/org/commcare/formplayer/api/json/PromptToJson.java b/src/main/java/org/commcare/formplayer/api/json/PromptToJson.java index 89d429b0c..b5ba37837 100644 --- a/src/main/java/org/commcare/formplayer/api/json/PromptToJson.java +++ b/src/main/java/org/commcare/formplayer/api/json/PromptToJson.java @@ -3,6 +3,8 @@ import org.commcare.formplayer.exceptions.ApplicationConfigException; import org.javarosa.core.model.Constants; import org.javarosa.core.model.FormIndex; +import org.javarosa.core.model.GroupDef; +import org.javarosa.core.model.IFormElement; import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.IAnswerData; @@ -10,6 +12,8 @@ import org.javarosa.core.model.data.helper.Selection; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.utils.DateUtils; +import org.javarosa.core.services.locale.Localization; +import org.javarosa.core.util.NoLocalizedTextException; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; @@ -110,18 +114,39 @@ public static JSONObject parseQuestionType(FormEntryModel model, JSONObject obj) obj.put("type", "sub-group"); obj.put("repeatable", true); obj.put("exists", true); + obj.put("delete", model.isNonCountedRepeat()); break; case FormEntryController.EVENT_PROMPT_NEW_REPEAT: - // we're in a subgroup - parseCaption(model.getCaptionPrompt(), obj); + // we're in a subgroup, dummy node for user counted repeat group + FormEntryCaption prompt = model.getCaptionPrompt(); + parseCaption(prompt, obj); obj.put("type", "sub-group"); obj.put("repeatable", true); obj.put("exists", false); + obj.put("delete", false); + obj.put("add-choice", getRepeatAddText(prompt)); break; } return obj; } + private static String getRepeatAddText(FormEntryCaption prompt) { + String promptText = prompt.getLongText(); + if (prompt.getNumRepetitions() > 0) { + try { + return Localization.get("repeat.dialog.add.another", promptText); + } catch (NoLocalizedTextException e) { + return "Add another " + promptText; + } + } else { + try { + return Localization.get("repeat.dialog.add.new", promptText); + } catch (NoLocalizedTextException e) { + return "Add a new " + promptText; + } + } + } + private static void parseRepeatJuncture(FormEntryModel model, JSONObject obj, FormIndex ix) { FormEntryCaption formEntryCaption = model.getCaptionPrompt(ix); FormEntryCaption.RepeatOptions repeatOptions = formEntryCaption.getRepeatOptions(); diff --git a/src/main/java/org/commcare/formplayer/application/FormController.java b/src/main/java/org/commcare/formplayer/application/FormController.java index 1d32498d1..35169e7cc 100644 --- a/src/main/java/org/commcare/formplayer/application/FormController.java +++ b/src/main/java/org/commcare/formplayer/application/FormController.java @@ -283,7 +283,7 @@ public FormEntryResponseBean deleteRepeat(@RequestBody RepeatRequestBean deleteR FormSession formEntrySession = formSessionFactory.getFormSession(serializableFormSession); JSONObject response = JsonActionUtils.deleteRepeatToJson(formEntrySession.getFormEntryController(), formEntrySession.getFormEntryModel(), - deleteRepeatRequestBean.getRepeatIndex(), deleteRepeatRequestBean.getFormIndex()); + deleteRepeatRequestBean.getRepeatIndex()); updateSession(formEntrySession); FormEntryResponseBean responseBean = mapper.readValue(response.toString(), FormEntryResponseBean.class); responseBean.setTitle(serializableFormSession.getTitle()); diff --git a/src/main/java/org/commcare/formplayer/beans/QuestionBean.java b/src/main/java/org/commcare/formplayer/beans/QuestionBean.java index e90cd5289..dd80ed911 100644 --- a/src/main/java/org/commcare/formplayer/beans/QuestionBean.java +++ b/src/main/java/org/commcare/formplayer/beans/QuestionBean.java @@ -34,6 +34,8 @@ public class QuestionBean { private String repeatable; private String exists; private String addChoice; + + private boolean delete; private String header; private int control; @@ -248,6 +250,14 @@ public void setAddChoice(String addChoice) { this.addChoice = addChoice; } + public boolean isDelete() { + return delete; + } + + public void setDelete(boolean delete) { + this.delete = delete; + } + public String getHeader() { return header; } diff --git a/src/main/java/org/commcare/formplayer/beans/RepeatRequestBean.java b/src/main/java/org/commcare/formplayer/beans/RepeatRequestBean.java index 5a4e69503..1eff60340 100644 --- a/src/main/java/org/commcare/formplayer/beans/RepeatRequestBean.java +++ b/src/main/java/org/commcare/formplayer/beans/RepeatRequestBean.java @@ -10,7 +10,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class RepeatRequestBean extends SessionRequestBean { private String repeatIndex; - private String formIndex; // our JSON-Object mapping lib (Jackson) requires a default constructor public RepeatRequestBean() { @@ -31,14 +30,4 @@ public void setRepeatIndex(String repeatIndex) { public String toString() { return "RepeatRequestBean [repeatIndex: " + repeatIndex + ", sessionId: " + sessionId + "]"; } - - @JsonGetter(value = "form_ix") - public String getFormIndex() { - return formIndex; - } - - @JsonSetter(value = "form_ix") - public void setFormIndex(String formIndex) { - this.formIndex = formIndex; - } } diff --git a/src/main/java/org/commcare/formplayer/session/FormSession.java b/src/main/java/org/commcare/formplayer/session/FormSession.java index 09586eaa8..fb02679e7 100644 --- a/src/main/java/org/commcare/formplayer/session/FormSession.java +++ b/src/main/java/org/commcare/formplayer/session/FormSession.java @@ -180,7 +180,7 @@ public FormSession(UserSqlSandbox sandbox, @Trace private void setupJavaRosaObjects() { - formEntryModel = new FormEntryModel(formDef, FormEntryModel.REPEAT_STRUCTURE_NON_LINEAR); + formEntryModel = new FormEntryModel(formDef, FormEntryModel.REPEAT_STRUCTURE_LINEAR); formEntryController = new FormEntryController(formEntryModel); formController = new FormController(formEntryController, false); langs = formEntryModel.getLanguages(); diff --git a/src/test/java/org/commcare/formplayer/tests/BaseTestClass.java b/src/test/java/org/commcare/formplayer/tests/BaseTestClass.java index d74f3b1d1..41d97c404 100644 --- a/src/test/java/org/commcare/formplayer/tests/BaseTestClass.java +++ b/src/test/java/org/commcare/formplayer/tests/BaseTestClass.java @@ -605,11 +605,10 @@ NotificationMessage deleteApplicationDbs() throws Exception { ); } - FormEntryResponseBean newRepeatRequest(String sessionId) throws Exception { - String newRepeatRequestPayload = FileUtils.getFile(this.getClass(), - "requests/new_repeat/new_repeat.json"); - RepeatRequestBean newRepeatRequestBean = mapper.readValue(newRepeatRequestPayload, - RepeatRequestBean.class); + FormEntryResponseBean newRepeatRequest(String sessionId, String repeatIndex) throws Exception { + RepeatRequestBean newRepeatRequestBean = new RepeatRequestBean(); + newRepeatRequestBean.setRepeatIndex(repeatIndex); + newRepeatRequestBean.setSessionId(sessionId); populateFromSession(newRepeatRequestBean, sessionId); return generateMockQuery( @@ -621,13 +620,11 @@ FormEntryResponseBean newRepeatRequest(String sessionId) throws Exception { ); } - FormEntryResponseBean deleteRepeatRequest(String sessionId) throws Exception { - - String newRepeatRequestPayload = FileUtils.getFile(this.getClass(), - "requests/delete_repeat/delete_repeat.json"); - - RepeatRequestBean deleteRepeatRequest = mapper.readValue(newRepeatRequestPayload, - RepeatRequestBean.class); + FormEntryResponseBean deleteRepeatRequest(String sessionId, String repeatIndex) + throws Exception { + RepeatRequestBean deleteRepeatRequest = new RepeatRequestBean(); + deleteRepeatRequest.setRepeatIndex(repeatIndex); + deleteRepeatRequest.setSessionId(sessionId); populateFromSession(deleteRepeatRequest, sessionId); return generateMockQuery( ControllerType.FORM, diff --git a/src/test/java/org/commcare/formplayer/tests/RepeatTests.java b/src/test/java/org/commcare/formplayer/tests/RepeatTests.java index 7a16be457..904b88485 100644 --- a/src/test/java/org/commcare/formplayer/tests/RepeatTests.java +++ b/src/test/java/org/commcare/formplayer/tests/RepeatTests.java @@ -1,6 +1,8 @@ package org.commcare.formplayer.tests; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.commcare.formplayer.beans.FormEntryResponseBean; import org.commcare.formplayer.beans.NewFormResponse; @@ -35,66 +37,161 @@ public void setUp() throws Exception { } @Test - public void testRepeat() throws Exception { - + public void testRepeatNonCountedSimple() throws Exception { NewFormResponse newSessionResponse = startNewForm("requests/new_form/new_form.json", "xforms/repeat.xml"); + QuestionBean[] tree = newSessionResponse.getTree(); + assert (tree.length == 2); + QuestionBean dummyNode = tree[1]; + assertEquals("false", dummyNode.getExists()); + assertEquals("Add a new question3", dummyNode.getAddChoice()); + assertEquals("false", dummyNode.getExists()); + assertEquals(false, dummyNode.isDelete()); String sessionId = newSessionResponse.getSessionId(); + FormEntryResponseBean newRepeatResponseBean = newRepeatRequest(sessionId, "1_0"); - FormEntryResponseBean newRepeatResponseBean = newRepeatRequest(sessionId); - - QuestionBean[] tree = newRepeatResponseBean.getTree(); - - assert (tree.length == 2); - QuestionBean questionBean = tree[1]; - assert (questionBean.getChildren() != null); - QuestionBean[] children = questionBean.getChildren(); + // Verify the repeat has been added to form tree correctly + tree = newRepeatResponseBean.getTree(); + assert (tree.length == 3); + QuestionBean firstRepeat = tree[1]; + assertEquals("true", firstRepeat.getExists()); + assertEquals(true, firstRepeat.isDelete()); + assert (firstRepeat.getChildren().length == 1); + QuestionBean[] children = firstRepeat.getChildren(); assert (children.length == 1); QuestionBean child = children[0]; - assert (child.getIx().contains("1_0")); - children = child.getChildren(); - assert (children.length == 1); - child = children[0]; assert (child.getIx().contains("1_0,0")); + // verify that a second dummy repeat node is added with "exists=false" + QuestionBean secondRepeat = tree[2]; + assertEquals("false", secondRepeat.getExists()); + assertEquals(false, secondRepeat.isDelete()); + assert (secondRepeat.getChildren().length == 0); + assertEquals("Add another question3", secondRepeat.getAddChoice()); - newRepeatResponseBean = newRepeatRequest(sessionId); - + // Add another repeat and verify the form tree accordingly + newRepeatResponseBean = newRepeatRequest(sessionId, "1_1"); tree = newRepeatResponseBean.getTree(); - assert (tree.length == 2); - questionBean = tree[1]; - assert (questionBean.getChildren() != null); - children = questionBean.getChildren(); - assert (children.length == 2); + assert (tree.length == 4); + secondRepeat = tree[2]; + assertEquals("true", secondRepeat.getExists()); + assert (secondRepeat.getChildren().length == 1); + children = secondRepeat.getChildren(); + assert (children[0].getIx().contains("1_1,0")); + + QuestionBean thirdRepeat = tree[3]; + assertEquals("false", thirdRepeat.getExists()); + assert (thirdRepeat.getChildren().length == 0); + + newRepeatRequest(sessionId, "1_2"); + answerQuestionGetResult("1_0,0", "repeat 1", sessionId); + answerQuestionGetResult("1_1,0", "repeat 2", sessionId); + answerQuestionGetResult("1_2,0", "repeat 3", sessionId); + + // delete second repeat + FormEntryResponseBean deleteRepeatResponseBean = deleteRepeatRequest(sessionId,"1_1,0"); + + // Verify that we deleted the repeat at right index + tree = deleteRepeatResponseBean.getTree(); + assert (tree.length == 4); + firstRepeat = tree[1]; + secondRepeat = tree[2]; + thirdRepeat = tree[3]; + assertEquals(firstRepeat.getChildren()[0].getAnswer(),"repeat 1"); + assertEquals(secondRepeat.getChildren()[0].getAnswer(),"repeat 3"); + assertEquals("false", thirdRepeat.getExists()); - child = children[0]; - assert (child.getIx().contains("1_0")); - QuestionBean[] children2 = child.getChildren(); - assert (children2.length == 1); - child = children2[0]; - assert (child.getIx().contains("1_0,0")); - child = children[1]; - children2 = child.getChildren(); - assert (children2.length == 1); - child = children2[0]; - assert (child.getIx().contains("1_1,0")); + // delete second repeat again from the new tree + deleteRepeatResponseBean = deleteRepeatRequest(sessionId, "1_1,0"); + tree = deleteRepeatResponseBean.getTree(); + assert (tree.length == 3); + firstRepeat = tree[1]; + secondRepeat = tree[2]; + assertEquals(firstRepeat.getChildren()[0].getAnswer(),"repeat 1"); + assertEquals("false", secondRepeat.getExists()); + } - FormEntryResponseBean deleteRepeatResponseBean = deleteRepeatRequest(sessionId); + @Test + public void testRepeatNonCountedNested() throws Exception { + NewFormResponse newSessionResponse = startNewForm("requests/new_form/new_form.json", + "xforms/nested_repeat.xml"); + QuestionBean[] tree = newSessionResponse.getTree(); + assert (tree.length == 1); + QuestionBean dummyNode = tree[0]; + assertEquals("false", dummyNode.getExists()); - tree = deleteRepeatResponseBean.getTree(); + String sessionId = newSessionResponse.getSessionId(); + FormEntryResponseBean newRepeatResponseBean = newRepeatRequest(sessionId, "0_0"); + tree = newRepeatResponseBean.getTree(); + + // Verify the repeat has been added to form tree correctly assert (tree.length == 2); - questionBean = tree[1]; - assert (questionBean.getChildren() != null); - children = questionBean.getChildren(); + QuestionBean firstRepeat = tree[0]; + assertEquals("true", firstRepeat.getExists()); + assert (firstRepeat.getChildren().length == 1); + QuestionBean[] children = firstRepeat.getChildren(); assert (children.length == 1); + QuestionBean child = children[0]; + assertEquals(child.getIx(), "0_0,0_0"); + assertEquals("false", child.getExists()); + + // Child repeat request + newRepeatResponseBean = newRepeatRequest(sessionId, "0_0, 0_0"); + tree = newRepeatResponseBean.getTree(); + assert (tree.length == 2); + children = tree[0].getChildren(); + assert (children.length == 2); + QuestionBean firstChild = children[0]; + assertEquals(firstChild.getIx(), "0_0,0_0"); + assertEquals("true", firstChild.getExists()); + QuestionBean secondChild = children[1]; + assertEquals(secondChild.getIx(), "0_0,0_1"); + assertEquals("false", secondChild.getExists()); + + newRepeatRequest(sessionId, "0_0, 0_1"); + + // create another parent repeat with three children + newRepeatRequest(sessionId, "0_1"); + newRepeatRequest(sessionId, "0_1, 0_0"); + newRepeatRequest(sessionId, "0_1, 0_1"); + tree = newRepeatRequest(sessionId, "0_1, 0_2").getTree(); + + // verify the form tree has 2 parent node with 2 and 3 children respectively + // count below is count + 1 to account for dummy node + assertEquals(3, tree.length); + assertEquals (3, tree[0].getChildren().length); + assertEquals (4, tree[1].getChildren().length); + + // Answer repeats + answerQuestionGetResult("0_0,0_0,0", "repeat 1_1", sessionId); + answerQuestionGetResult("0_0,0_1,0", "repeat 1_2", sessionId); + answerQuestionGetResult("0_1,0_0,0", "repeat 2_1", sessionId); + answerQuestionGetResult("0_1,0_1,0", "repeat 2_2", sessionId); + answerQuestionGetResult("0_1,0_2,0", "repeat 2_3", sessionId); + + // delete 1_1 and 2_2 and verify the state + deleteRepeatRequest(sessionId, "0_0,0_0"); + tree = deleteRepeatRequest(sessionId, "0_1,0_1").getTree(); + assertEquals(3, tree.length); + assertEquals (2, tree[0].getChildren().length); + assertEquals (3, tree[1].getChildren().length); + assertEquals(tree[0].getChildren()[0].getChildren()[0].getAnswer(),"repeat 1_2"); + assertEquals(tree[0].getChildren()[1].getExists(),"false"); + assertEquals(tree[1].getChildren()[0].getChildren()[0].getAnswer(),"repeat 2_1"); + assertEquals(tree[1].getChildren()[1].getChildren()[0].getAnswer(),"repeat 2_3"); + assertEquals(tree[1].getChildren()[2].getExists(),"false"); } @Test public void testRepeatModelIteration() throws Exception { NewFormResponse newSessionResponse = startNewForm("requests/new_form/new_form.json", "xforms/repeat_model_iteration.xml"); + + // counted repeat group nodes can't be deleted + assertFalse(newSessionResponse.getTree()[1].isDelete()); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new ByteArrayInputStream( diff --git a/src/test/resources/xforms/nested_repeat.xml b/src/test/resources/xforms/nested_repeat.xml new file mode 100644 index 000000000..6fb628c4c --- /dev/null +++ b/src/test/resources/xforms/nested_repeat.xml @@ -0,0 +1,51 @@ + + + Nested Non Counted + + + + + + + + + + + + + + + + + + T1 + + + RG Non Counted + + + Nested Repeat + + + T + + + + + + + + + +