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

#45 repeat-groups are not respecting the jr:count setting after a change to a lower value #61

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa">
<h:head>
<h:title>limited repeats count</h:title>
<model>
<instance>
<data id="limited-repeats-count">
<repeat_count/>
<limited_repeat>
<free_text_1/>
<free_text_2/>
</limited_repeat>
<non_relevant_text/>
</data>
</instance>
<bind nodeset="/data/repeat_count" type="int"/>
<bind nodeset="/data/limited_repeat" relevant="/data/repeat_count > 0"/>
<bind nodeset="/data/limited_repeat/free_text_1" type="string"/>
<bind nodeset="/data/limited_repeat/free_text_2" type="string"/>
<bind nodeset="/data/non_relevant_text" type="string" relevant="false()"/>
</model>
</h:head>
<h:body>
<input ref="/data/repeat_count">
<label>repeat count</label>
</input>
<group>
<label>limited repeat</label>
<repeat nodeset="/data/limited_repeat" jr:count="/data/repeat_count"
jr:noAddRemove="true()" appearance="field-list">
<input ref="/data/limited_repeat/free_text_1">
<label>free text 1</label>
</input>
<input ref="/data/limited_repeat/free_text_2">
<label>free text 2</label>
</input>
</repeat>
</group>
<input ref="/data/non_relevant_text">
<label>should not be visible</label>
</input>
</h:body>
</h:html>
38 changes: 38 additions & 0 deletions resources/limited-repeats-count.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<h:html xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa">
<h:head>
<h:title>limited repeats count</h:title>
<model>
<instance>
<data id="limited-repeats-count">
<repeat_count/>
<limited_repeat>
<free_text_1/>
<free_text_2/>
</limited_repeat>
</data>
</instance>
<bind nodeset="/data/repeat_count" type="int"/>
<bind nodeset="/data/limited_repeat" relevant="/data/repeat_count > 0"/>
<bind nodeset="/data/limited_repeat/free_text_1" type="string"/>
<bind nodeset="/data/limited_repeat/free_text_2" type="string"/>
</model>
</h:head>
<h:body>
<input ref="/data/repeat_count">
<label>repeat count</label>
</input>
<group>
<label>limited repeat</label>
<repeat nodeset="/data/limited_repeat" jr:count="/data/repeat_count"
jr:noAddRemove="true()" appearance="field-list">
<input ref="/data/limited_repeat/free_text_1">
<label>free text 1</label>
</input>
<input ref="/data/limited_repeat/free_text_2">
<label>free text 2</label>
</input>
</repeat>
</group>
</h:body>
</h:html>
15 changes: 13 additions & 2 deletions src/org/javarosa/form/api/FormEntryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,18 @@ private int stepEvent(boolean forward) {
}
} while (index.isInForm() && !model.isIndexRelevant(index));

return jumpToIndex(index);
int event = jumpToIndex(index);

/* The above call may update the model (add or remove repeat groups) which may result in
* the model's current form index pointing to the first form element after
* the deleted repeat group which in fact may be non-relevant so it should be
* jumped over.
*/
if (!model.getFormIndex().isInForm() || model.isIndexRelevant()) {
return event;
} else {
return stepEvent(forward);
}
}


Expand All @@ -208,7 +219,7 @@ private int stepEvent(boolean forward) {
*/
public int jumpToIndex(FormIndex index) {
model.setQuestionIndex(index);
return model.getEvent(index);
return model.getEvent(model.getFormIndex());
}

public FormIndex descendIntoRepeat (int n) {
Expand Down
98 changes: 67 additions & 31 deletions src/org/javarosa/form/api/FormEntryModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,7 @@ public void setQuestionIndex(FormIndex index) {
if (!currentFormIndex.equals(index)) {
// See if a hint exists that says we should have a model for this
// already
createModelIfNecessary(index);
currentFormIndex = index;
currentFormIndex = updateModelIfNecessary(index);
}
}

Expand Down Expand Up @@ -425,7 +424,9 @@ public boolean isIndexRelevant() {


/**
* For the current index: Checks whether the index represents a node which
* For the current index:
*
* 1. Checks whether the index represents a node which
* should exist given a non-interactive repeat, along with a count for that
* repeat which is beneath the dynamic level specified.
*
Expand All @@ -437,40 +438,75 @@ public boolean isIndexRelevant() {
* the interface, it will merely use the xforms repeat hint to create new
* nodes that are assumed to exist
*
* 2. Checks whether the index represents a node which should have been deleted because
* the count for that repeat had been changed to a lower value.
* If this index does represent such a node, this and all further extraneous
* repeat groups are deleted and it returns the first index after the last deleted repeat group.
*
* @param index The index to be evaluated as to whether the underlying model is
* hinted to exist
* @return The current index after the model update
*/
private void createModelIfNecessary(FormIndex index) {
if (index.isInForm()) {
IFormElement e = getForm().getChild(index);
if (e instanceof GroupDef) {
GroupDef g = (GroupDef) e;
if (g.getRepeat() && g.getCountReference() != null) {
// Lu Gram: repeat count XPath needs to be contextualized for nested repeat groups
TreeReference countRef = FormInstance.unpackReference(g.getCountReference());
TreeReference contextualized = countRef.contextualize(index.getReference());
IAnswerData count = getForm().getMainInstance().resolveReference(contextualized).getValue();
if (count != null) {
long fullcount = ((Integer) count.getValue()).intValue();
TreeReference ref = getForm().getChildInstanceRef(index);
TreeElement element = getForm().getMainInstance().resolveReference(ref);
if (element == null) {
if (index.getTerminal().getInstanceIndex() < fullcount) {

try {
getForm().createNewRepeat(index);
} catch (InvalidReferenceException ire) {
ire.printStackTrace();
throw new RuntimeException("Invalid Reference while creting new repeat!" + ire.getMessage());
}
}
}
}
}

private FormIndex updateModelIfNecessary(FormIndex index) {
if (!index.isInForm()) {
return index;
}
IFormElement e = getForm().getChild(index);
if (!(e instanceof GroupDef)) {
return index;
}
GroupDef g = (GroupDef) e;
if (!g.getRepeat() || g.getCountReference() == null) {
return index;
}
// Lu Gram: repeat count XPath needs to be contextualized for nested repeat groups
TreeReference countRef = FormInstance.unpackReference(g.getCountReference());
TreeReference contextualized = countRef.contextualize(index.getReference());
IAnswerData count = getForm().getMainInstance().resolveReference(contextualized).getValue();
if (count == null) {
return index;
}

long fullCount = (Integer) count.getValue();
TreeReference ref = getForm().getChildInstanceRef(index);
TreeElement element = getForm().getMainInstance().resolveReference(ref);
int currentInstanceIndex = index.getTerminal().getInstanceIndex();

if (currentInstanceIndex < fullCount && element == null) {
// Create the missing repeat group.
try {
getForm().createNewRepeat(index);
} catch (InvalidReferenceException ire) {
ire.printStackTrace();
throw new RuntimeException("Invalid Reference while creating a new repeat!" + ire.getMessage());
}
} else if (currentInstanceIndex >= fullCount && element != null) {
/*
* Delete the excessive repeat groups.
*
* When a repeat group is deleted then all the following groups are shifted to fill the gap.
* Example:
* There are 6 repeat groups and the 4th is deleted.
* The 5th becomes the 4th and the 6th becomes the 5th. The index still points to
* the 4th repeat group which used to be the 5th so the form element represented by
* the index still exists.
*
* The loop keeps deleting repeat groups pointed by the index
* (so in the above example it would be the 4th) till the last one is deleted.
*/
do {
getForm().deleteRepeat(index);
} while (getForm().getMainInstance().resolveReference(index.getReference()) != null);

/* At this moment index points to a non-existing repeat group so we must
* leave it by stepping to the next form index.
*/
return incrementIndex(index);
}
}

return index;
}

public boolean isIndexCompoundContainer() {
return isIndexCompoundContainer(getFormIndex());
Expand Down
87 changes: 87 additions & 0 deletions test/org/javarosa/core/form/api/test/LimitedRepeatCountTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.javarosa.core.form.api.test;


import org.javarosa.core.PathConst;
import org.javarosa.core.model.data.IntegerData;
import org.javarosa.core.test.FormParseInit;
import org.javarosa.form.api.FormEntryController;
import org.javarosa.form.api.FormEntryPrompt;
import org.junit.Test;

import java.nio.file.Paths;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class LimitedRepeatCountTests {

private static final int TEN_REPEAT_COUNT = 10;
private static final int NINE_REPEAT_COUNT = 9;
private static final int FIVE_REPEAT_COUNT = 5;

@Test
public void testRemoveExtraneousRepeatGroups() {
FormParseInit formParseInit = new FormParseInit();
formParseInit.setFormToParse(Paths.get(PathConst.getTestResourcePath().getAbsolutePath(), "limited-repeats-count.xml").toString());

FormEntryController formEntryController = formParseInit.getFormEntryController();

testSteps(formEntryController);
}

@Test
public void testRemoveExtraneousRepeatGroups_withNonRelevantIndexAfterRepeatGroup() {
FormParseInit formParseInit = new FormParseInit();
formParseInit.setFormToParse(Paths.get(PathConst.getTestResourcePath().getAbsolutePath(), "limited-repeats-count-non-relevant-index-after-repeat-group.xml").toString());

FormEntryController formEntryController = formParseInit.getFormEntryController();

testSteps(formEntryController);
}

/**
* Steps are shared between {@link LimitedRepeatCountTests#testRemoveExtraneousRepeatGroups()}
* and {@link LimitedRepeatCountTests#testRemoveExtraneousRepeatGroups_withNonRelevantIndexAfterRepeatGroup()}.
* The only difference is in forms they use.
* @param formEntryController
*/
private void testSteps(FormEntryController formEntryController) {
assertTrue(formEntryController.getModel().getFormIndex().isBeginningOfFormIndex());

formEntryController.stepToNextEvent();

FormEntryPrompt questionPrompt = formEntryController.getModel().getQuestionPrompt();

assertEquals("/data/repeat_count", questionPrompt.getQuestion().getBind().getReference().toString());
formEntryController.answerQuestion(questionPrompt.getIndex(), new IntegerData(TEN_REPEAT_COUNT), true);

for (int i = 0; i < TEN_REPEAT_COUNT; i++) {
assertEquals(FormEntryController.EVENT_REPEAT, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
}
assertEquals(FormEntryController.EVENT_END_OF_FORM, formEntryController.stepToNextEvent());

formEntryController.jumpToIndex(questionPrompt.getIndex());
assertEquals("/data/repeat_count", questionPrompt.getQuestion().getBind().getReference().toString());
formEntryController.answerQuestion(questionPrompt.getIndex(), new IntegerData(NINE_REPEAT_COUNT), true);

for (int i = 0; i < NINE_REPEAT_COUNT; i++) {
assertEquals(FormEntryController.EVENT_REPEAT, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
}
assertEquals(FormEntryController.EVENT_END_OF_FORM, formEntryController.stepToNextEvent());

formEntryController.jumpToIndex(questionPrompt.getIndex());
assertEquals("/data/repeat_count", questionPrompt.getQuestion().getBind().getReference().toString());
formEntryController.answerQuestion(questionPrompt.getIndex(), new IntegerData(FIVE_REPEAT_COUNT), true);

for (int i = 0; i < FIVE_REPEAT_COUNT; i++) {
assertEquals(FormEntryController.EVENT_REPEAT, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
assertEquals(FormEntryController.EVENT_QUESTION, formEntryController.stepToNextEvent());
}
assertEquals(FormEntryController.EVENT_END_OF_FORM, formEntryController.stepToNextEvent());
}
}