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

Include locally created entities in follow up forms #5982

Merged
merged 41 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
09a44e2
Spiked version of offline entities in mem
seadowg Feb 22, 2024
401fe39
Use processor to implement offline entities
seadowg Feb 22, 2024
c1ee90e
Move offline entities code to module
seadowg Feb 22, 2024
7c7c894
Handle duplicates in entity lists
seadowg Feb 22, 2024
0d54485
Make properties available in follow up forms
seadowg Feb 22, 2024
bd14962
Fix test
seadowg Feb 22, 2024
6857cf1
Persist entities
seadowg Feb 22, 2024
dd6641b
Make offline entities optional
seadowg Feb 22, 2024
ae37cb5
Add failing test for form cache problems
seadowg Feb 22, 2024
ece5099
Don't cache forms with external secondary instances
seadowg Feb 22, 2024
3d5e961
Override external instance parsing instead to fix caching problem
seadowg Feb 23, 2024
43ec1ea
Make local entities opt in
seadowg Feb 23, 2024
ee086b5
Simplify update form
seadowg Feb 27, 2024
a12456d
Add test for entity update
seadowg Feb 27, 2024
aec9b83
Implement correct update behaviour for properties
seadowg Feb 27, 2024
294c9be
Handle update forms without label
seadowg Feb 27, 2024
0134135
Use server setup for entities testing
seadowg Feb 27, 2024
4a2d15f
Allow overriding file names in StubOpenRosaServer
seadowg Feb 27, 2024
f8ae140
Handle duplicate updates for entities
seadowg Feb 27, 2024
9a07f69
Add label to entity browser
seadowg Mar 1, 2024
4dbdd9b
Reorganize experimental preferences
seadowg Mar 1, 2024
817be62
Add selectable background to blank form items
seadowg Mar 4, 2024
f1372f6
Use recycler view in entity browser
seadowg Mar 4, 2024
4d753bb
Move entity loading to background thread
seadowg Mar 4, 2024
32c5441
Store entities.json in project dir
seadowg Mar 11, 2024
3d33e4d
Add clear action for entities
seadowg Mar 11, 2024
21cf07b
Use Toolbar as MenuHost
seadowg Mar 11, 2024
d021205
Use simple objects for GSON to prevent proguard problems
seadowg Mar 14, 2024
a0703e1
Use contains() in test
seadowg Mar 27, 2024
805adc0
Verify that null labels update properties
seadowg Mar 27, 2024
32f0b9e
Switch default for method
seadowg Mar 27, 2024
6e4955b
Rename test
seadowg Mar 27, 2024
1d415f3
Make sure version is specified in test
seadowg Mar 27, 2024
30b252b
Move element names to constants object
seadowg Mar 27, 2024
3a9797c
Added test for blank offline entity label behaviour
seadowg Mar 27, 2024
666ee51
Remove unneeded assertions
seadowg Mar 27, 2024
6f281b0
Update test names
seadowg Mar 27, 2024
3968839
Use normal case where versions don't match for testing entities repos…
seadowg Mar 27, 2024
6a33b9e
Delete test that duplicates coverage
seadowg Mar 27, 2024
faf4f10
Use copy to make test clearer
seadowg Mar 29, 2024
9f4e7cc
Make sure that online properties are included in merge
seadowg Mar 29, 2024
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
Expand Up @@ -20,8 +20,8 @@ open class CoroutineScheduler(private val foregroundContext: CoroutineContext, p
}
}

override fun immediate(background: Boolean, runnable: Runnable) {
val context = if (background) {
override fun immediate(foreground: Boolean, runnable: Runnable) {
val context = if (!foreground) {
backgroundContext
} else {
foregroundContext
Expand Down
2 changes: 1 addition & 1 deletion async/src/main/java/org/odk/collect/async/Scheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface Scheduler {
/**
* Run work in the foreground or background. Cancelled if application closed.
*/
fun immediate(background: Boolean = false, runnable: Runnable)
fun immediate(foreground: Boolean = false, runnable: Runnable)

/**
* Schedule a task to run in the background even if the app isn't running. The task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ abstract class SchedulerAsyncTaskMimic<Params, Progress, Result>(private val sch
}

protected fun publishProgress(vararg values: Progress) {
scheduler.immediate(
runnable = { onProgressUpdate(*values) }
)
scheduler.immediate(foreground = true) {
onProgressUpdate(*values)
}
}
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/java/dependencies/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ object Dependencies {
const val rarepebble_colorpicker = "com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0"
const val commons_io = "commons-io:commons-io:2.5" // Commons 2.6+ introduce java.nio usage that we can't access until our minSdkVersion >= 26 (https://developer.android.com/reference/java/io/File#toPath())
const val opencsv = "com.opencsv:opencsv:5.9"
const val javarosa_online = "org.getodk:javarosa:4.3.2"
const val javarosa_online = "org.getodk:javarosa:4.4.0-SNAPSHOT"
const val javarosa_local = "org.getodk:javarosa:local"
const val javarosa = javarosa_online
const val karumi_dexter = "com.karumi:dexter:6.2.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.odk.collect.android.feature.entitymanagement

import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.odk.collect.android.support.TestDependencies
import org.odk.collect.android.support.pages.FormEntryPage
import org.odk.collect.android.support.rules.CollectTestRule
import org.odk.collect.android.support.rules.TestRuleChain
import org.odk.collect.strings.R.string

class DeleteEntitiesTest {

private val rule = CollectTestRule(useDemoProject = false)
private val testDependencies = TestDependencies()

@get:Rule
val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)
.around(rule)

@Test
fun canClearAllEntities() {
testDependencies.server.addForm("one-question-entity-registration.xml")

rule.withMatchExactlyProject(testDependencies.server.url)
.startBlankForm("One Question Entity Registration")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Logan Roy"))
.openEntityBrowser()
.clickOptionsIcon(string.clear_entities)
.clickOnString(string.clear_entities)
.assertTextDoesNotExist("people")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,139 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.odk.collect.android.support.TestDependencies
import org.odk.collect.android.support.pages.FormEntryPage
import org.odk.collect.android.support.rules.CollectTestRule
import org.odk.collect.android.support.rules.TestRuleChain

@RunWith(AndroidJUnit4::class)
class EntityFormTest {

private val rule = CollectTestRule()
private val rule = CollectTestRule(useDemoProject = false)
private val testDependencies = TestDependencies()

@get:Rule
val ruleChain: RuleChain = TestRuleChain.chain()
val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)
.around(rule)

@Test
fun fillingFormWithEntityCreateElement_createsAnEntity() {
rule.startAtMainMenu()
.copyForm("one-question-entity.xml")
.startBlankForm("One Question Entity")
fun fillingEntityRegistrationForm_createsEntityInTheBrowser() {
testDependencies.server.addForm("one-question-entity-registration.xml")

rule.withMatchExactlyProject(testDependencies.server.url)
.startBlankForm("One Question Entity Registration")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Logan Roy"))
.openEntityBrowser()
.clickOnDataset("people")
.assertEntity("full_name: Logan Roy")
.assertEntity("Logan Roy", "full_name: Logan Roy")
}

@Test
fun fillingEntityRegistrationForm_createsEntityForFollowUpForms() {
testDependencies.server.addForm("one-question-entity-registration.xml")
testDependencies.server.addForm("one-question-entity-update.xml", listOf("people.csv"))

rule.withMatchExactlyProject(testDependencies.server.url)
.enableLocalEntitiesInForms()

.startBlankForm("One Question Entity Registration")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Logan Roy"))

.startBlankForm("One Question Entity Update")
.assertQuestion("Select person")
.assertText("Roman Roy")
.assertText("Logan Roy")
}

@Test
fun fillingEntityRegistrationForm_createsEntityForFollowUpFormsWithCachedFormDefs() {
testDependencies.server.addForm("one-question-entity-registration.xml")
testDependencies.server.addForm("one-question-entity-update.xml", listOf("people.csv"))

rule.withMatchExactlyProject(testDependencies.server.url)
.enableLocalEntitiesInForms()

.startBlankForm("One Question Entity Update") // Open to create cached form def
.pressBackAndDiscardForm()

.startBlankForm("One Question Entity Registration")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Logan Roy"))

.startBlankForm("One Question Entity Update")
.assertQuestion("Select person")
.assertText("Roman Roy")
.assertText("Logan Roy")
}

@Test
fun fillingEntityUpdateForm_updatesEntityForFollowUpForms() {
testDependencies.server.addForm("one-question-entity-update.xml", listOf("people.csv"))

rule.withMatchExactlyProject(testDependencies.server.url)
.enableLocalEntitiesInForms()

.startBlankForm("One Question Entity Update")
.assertQuestion("Select person")
.clickOnText("Roman Roy")
.swipeToNextQuestion("Name")
.answerQuestion("Name", "Romulus Roy")
.swipeToEndScreen()
.clickFinalize()

.startBlankForm("One Question Entity Update")
.assertText("Romulus Roy")
.assertTextDoesNotExist("Roman Roy")
}

@Test
fun fillingEntityFollowUpForm_whenOnlineDuplicateHasHigherVersion_showsOnlineVersion() {
grzesiek2010 marked this conversation as resolved.
Show resolved Hide resolved
testDependencies.server.addForm("one-question-entity-update.xml", listOf("people.csv"))

val mainMenuPage = rule.withMatchExactlyProject(testDependencies.server.url)
.enableLocalEntitiesInForms()

.startBlankForm("One Question Entity Update")
.assertQuestion("Select person")
.clickOnText("Roman Roy")
.swipeToNextQuestion("Name")
.answerQuestion("Name", "Romulus Roy")
.swipeToEndScreen()
.clickFinalize()

testDependencies.server.updateMediaFile(
"one-question-entity-update.xml",
"people.csv",
"updated-people.csv"
)

mainMenuPage.clickFillBlankForm()
.clickRefresh()
.clickOnForm("One Question Entity Update")
.assertText("Ro-Ro Roy")
.assertTextDoesNotExist("Romulus Roy")
.assertTextDoesNotExist("Roman Roy")
}

@Test
fun fillingEntityFollowUpForm_whenOfflineDuplicateHasHigherVersion_showsOfflineVersion() {
testDependencies.server.addForm(
"one-question-entity-update.xml",
mapOf("people.csv" to "updated-people.csv")
)

rule.withMatchExactlyProject(testDependencies.server.url)
.enableLocalEntitiesInForms()

.startBlankForm("One Question Entity Update")
.assertQuestion("Select person")
.clickOnText("Ro-Ro Roy")
.swipeToNextQuestion("Name")
.answerQuestion("Name", "Romulus Roy")
.swipeToEndScreen()
.clickFinalize()

.startBlankForm("One Question Entity Update")
.assertText("Romulus Roy")
.assertTextDoesNotExist("Ro-Ro Roy")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SwitchProjectTest {

@Test
fun switchingProject_switchesSettingsFormsInstancesAndEntities() {
testDependencies.server.addForm("One Question Entity", "one-question-entity", "1", "one-question-entity.xml")
testDependencies.server.addForm("One Question Entity Registration", "one-question-entity", "1", "one-question-entity-registration.xml")

rule.startAtMainMenu()
// Copy and fill form
Expand Down Expand Up @@ -76,15 +76,15 @@ class SwitchProjectTest {
.clickOKOnDialog(MainMenuPage())

// Fill form
.startBlankForm("One Question Entity")
.startBlankForm("One Question Entity Registration")
.fillOutAndFinalize(FormEntryPage.QuestionAndAnswer("Name", "Alice"))
.clickSendFinalizedForm(1)
.assertText("One Question Entity")
.assertText("One Question Entity Registration")
.pressBack(MainMenuPage())

.openEntityBrowser()
.clickOnDataset("people")
.assertEntity("full_name: Alice")
.assertEntity("Alice", "full_name: Alice")
.pressBack(EntitiesPage())
.pressBack(ExperimentalPage())
.pressBack(ProjectSettingsPage())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.odk.collect.android.utilities.FileUtils.getResourceAsStream;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -31,6 +30,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

public class StubOpenRosaServer implements OpenRosaHttpInterface {
Expand Down Expand Up @@ -75,7 +75,7 @@ public HttpGetResult executeGetRequest(@NonNull URI uri, @Nullable String conten
} else {
return new HttpGetResult(null, new HashMap<>(), "", 404);
}
} else if (uri.getPath().equals("/mediaFile")) {
} else if (uri.getPath().startsWith("/mediaFile")) {
return new HttpGetResult(getMediaFile(uri), new HashMap<>(), "", 200);
} else {
return new HttpGetResult(null, new HashMap<>(), "", 404);
Expand Down Expand Up @@ -136,6 +136,23 @@ public void addForm(String formLabel, String id, String version, String formXML,
forms.add(new XFormItem(formLabel, formXML, id, version, mediaFiles));
}

public void addForm(String formXML, List<String> mediaFiles) {
forms.add(new XFormItem(formXML, formXML, formXML, "1", mediaFiles));
}

public void addForm(String formXML, Map<String, String> mediaFiles) {
forms.add(new XFormItem(formXML, formXML, formXML, "1", mediaFiles));
}

public void addForm(String formXML) {
forms.add(new XFormItem(formXML, formXML, formXML, "1"));
}

public void updateMediaFile(String formXML, String name, String newFile) {
Optional<XFormItem> formToUpdate = forms.stream().filter((form) -> form.formXML.equals(formXML)).findFirst();
formToUpdate.get().getMediaFiles().put(name, newFile);
}

public void removeForm(String formLabel) {
forms.removeIf(xFormItem -> xFormItem.getFormLabel().equals(formLabel));
}
Expand Down Expand Up @@ -245,18 +262,18 @@ private InputStream getManifestResponse(@NonNull URI uri) throws IOException {
.append("<?xml version='1.0' encoding='UTF-8' ?>\n")
.append("<manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n");

for (String mediaFile : xformItem.getMediaFiles()) {
for (Map.Entry<String, String> mediaFile : xformItem.getMediaFiles().entrySet()) {
String mediaFileHash;

if (randomHash) {
mediaFileHash = RandomString.randomString(8);
} else {
mediaFileHash = Md5.getMd5Hash(getResourceAsStream("media/" + mediaFile));
mediaFileHash = Md5.getMd5Hash(getResourceAsStream("media/" + mediaFile.getValue()));
}

stringBuilder
.append("<mediaFile>")
.append("<filename>" + mediaFile + "</filename>\n");
.append("<filename>" + mediaFile.getKey() + "</filename>\n");

if (noHashPrefixInMediaFiles) {
stringBuilder.append("<hash>" + mediaFileHash + " </hash>\n");
Expand All @@ -265,7 +282,7 @@ private InputStream getManifestResponse(@NonNull URI uri) throws IOException {
}

stringBuilder
.append("<downloadUrl>" + getURL() + "/mediaFile?name=" + mediaFile + "</downloadUrl>\n")
.append("<downloadUrl>" + getURL() + "/mediaFile/" + formID + "/" + mediaFile.getKey() + "</downloadUrl>\n")
.append("</mediaFile>\n");
}

Expand All @@ -282,8 +299,11 @@ private InputStream getFormXML(String formID) throws IOException {

@NotNull
private InputStream getMediaFile(URI uri) throws IOException {
String mediaFileName = uri.getQuery().split("=")[1];
return getResourceAsStream("media/" + mediaFileName);
String formID = uri.getPath().split("/mediaFile/")[1].split("/")[0];
String mediaFileName = uri.getPath().split("/mediaFile/")[1].split("/")[1];
XFormItem xformItem = forms.get(Integer.parseInt(formID));
String actualFileName = xformItem.getMediaFiles().get(mediaFileName);
return getResourceAsStream("media/" + actualFileName);
}

private static class XFormItem {
Expand All @@ -292,13 +312,20 @@ private static class XFormItem {
private final String formXML;
private final String id;
private final String version;
private final List<String> mediaFiles;
private final Map<String, String> mediaFiles;

XFormItem(String formLabel, String formXML, String id, String version) {
this(formLabel, formXML, id, version, emptyList());
this(formLabel, formXML, id, version, new HashMap<>());
}

XFormItem(String formLabel, String formXML, String id, String version, List<String> mediaFiles) {
this(formLabel, formXML, id, version);
for (String mediaFile : mediaFiles) {
this.mediaFiles.put(mediaFile, mediaFile);
}
}

XFormItem(String formLabel, String formXML, String id, String version, Map<String, String> mediaFiles) {
this.formLabel = formLabel;
this.formXML = formXML;
this.id = id;
Expand All @@ -322,7 +349,7 @@ public String getID() {
return id;
}

public List<String> getMediaFiles() {
public Map<String, String> getMediaFiles() {
return mediaFiles;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ class TestScheduler : Scheduler, CoroutineDispatcher() {
}
}

override fun immediate(background: Boolean, runnable: Runnable) {
override fun immediate(foreground: Boolean, runnable: Runnable) {
increment()
wrappedScheduler.immediate(background) {
wrappedScheduler.immediate(foreground) {
runnable.run()
decrement()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ class DatasetPage(private val datasetName: String) : Page<DatasetPage>() {
return this
}

fun assertEntity(fields: String): DatasetPage {
assertText(fields)
fun assertEntity(label: String, properties: String): DatasetPage {
assertText(label)
assertText(properties)
return this
}
}
Loading