Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linear Repeat Structure Support for Formplayer #1567

Merged
merged 7 commits into from
Jun 7, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
29 changes: 27 additions & 2 deletions src/main/java/org/commcare/formplayer/api/json/PromptToJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
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;
import org.javarosa.core.model.data.SelectMultiData;
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;
Expand Down Expand Up @@ -110,18 +114,39 @@
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);

Check warning on line 137 in src/main/java/org/commcare/formplayer/api/json/PromptToJson.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/commcare/formplayer/api/json/PromptToJson.java#L137

Added line #L137 was not covered by tests
} catch (NoLocalizedTextException e) {
return "Add another " + promptText;
}
} else {
try {
return Localization.get("repeat.dialog.add.new", promptText);

Check warning on line 143 in src/main/java/org/commcare/formplayer/api/json/PromptToJson.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/commcare/formplayer/api/json/PromptToJson.java#L143

Added line #L143 was not covered by tests
} 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/commcare/formplayer/beans/QuestionBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
11 changes: 0 additions & 11 deletions src/main/java/org/commcare/formplayer/beans/RepeatRequestBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 9 additions & 12 deletions src/test/java/org/commcare/formplayer/tests/BaseTestClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
171 changes: 134 additions & 37 deletions src/test/java/org/commcare/formplayer/tests/RepeatTests.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(
Expand Down