From 4244589d794fc7e8bbcd91c7adccb54271ace3c6 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 19 May 2026 00:47:55 +0200 Subject: [PATCH 1/7] Log analytics event when Entity create/update fails to apply --- entities/build.gradle.kts | 1 + .../collect/entities/LocalEntityUseCases.kt | 6 ++ .../entities/analytics/AnalyticsEvents.kt | 5 ++ .../EntityFormFinalizationProcessor.kt | 7 ++ .../entities/LocalEntityUseCasesTest.kt | 44 ++++++++++ .../EntityFormFinalizationProcessorTest.kt | 87 +++++++++++++++++++ 6 files changed, 150 insertions(+) create mode 100644 entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt diff --git a/entities/build.gradle.kts b/entities/build.gradle.kts index 9fe4f5be585..56d7fdc7117 100644 --- a/entities/build.gradle.kts +++ b/entities/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(project(":strings")) implementation(project(":shared")) implementation(project(":androidshared")) + implementation(project(":analytics")) implementation(project(":material")) implementation(project(":async")) implementation(project(":lists")) diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index 58eccf08882..bf65ebe9aa7 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -2,6 +2,8 @@ package org.odk.collect.entities import org.apache.commons.csv.CSVRecord import org.javarosa.core.model.instance.SecondaryInstanceCSVParserBuilder +import org.odk.collect.analytics.Analytics +import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity import org.odk.collect.entities.javarosa.parse.EntitySchema @@ -31,6 +33,8 @@ object LocalEntityUseCases { val existing = entitiesRepository.findEntityById(formEntity.dataset, formEntity.id) if (existing != null) { saveUpdatedEntity(formEntity, existing, entitiesRepository) + } else { + Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "UPDATE_NO_MATCH") } } @@ -77,6 +81,8 @@ object LocalEntityUseCases { "Entities", "Failed to create dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" ) + + Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "CREATE_NO_LABEL") } } diff --git a/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt new file mode 100644 index 00000000000..a1435cf7d88 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt @@ -0,0 +1,5 @@ +package org.odk.collect.entities.analytics + +object AnalyticsEvents { + const val ENTITY_FAILURE = "EntityFailure" +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt index 8e378edb460..2020f13043b 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt @@ -4,6 +4,8 @@ import org.javarosa.core.model.instance.FormInstance import org.javarosa.core.model.instance.TreeReference import org.javarosa.form.api.FormEntryFinalizationProcessor import org.javarosa.form.api.FormEntryModel +import org.odk.collect.analytics.Analytics +import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.parse.EntityFormExtra import org.odk.collect.entities.javarosa.parse.SaveTo import org.odk.collect.entities.javarosa.parse.isV4UUID @@ -82,6 +84,11 @@ class EntityFormFinalizationProcessor : FormEntryFinalizationProcessor { return if (id.isV4UUID()) { FormEntity(action, dataset, id, label, fields) } else { + if (id.isNullOrBlank()) { + Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "NO_ID") + } else { + Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "INVALID_ID") + } null } } diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index b35af6bd5d8..02d6b1edf13 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -10,6 +10,8 @@ import org.hamcrest.text.IsBlankString.blankOrNullString import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.odk.collect.analytics.Analytics +import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity import org.odk.collect.entities.javarosa.finalization.InvalidEntity @@ -246,6 +248,48 @@ class LocalEntityUseCasesTest { ) } + @Test + fun `#updateLocalEntitiesFromForm logs UPDATE_NO_MATCH when existing entity not found on update`() { + val analytics = mock() + Analytics.setInstance(analytics) + + val formEntity = + FormEntity(EntityAction.UPDATE, "things", "id", "label", emptyList()) + val formEntities = EntitiesExtra(listOf(formEntity)) + + LocalEntityUseCases.updateLocalEntitiesFromForm( + formEntities, + entitiesRepository + ) + + verify(analytics).logEventWithParam( + AnalyticsEvents.ENTITY_FAILURE, + "failure_code", + "UPDATE_NO_MATCH" + ) + } + + @Test + fun `#updateLocalEntitiesFromForm logs CREATE_NO_LABEL when label is blank on create`() { + val analytics = mock() + Analytics.setInstance(analytics) + + val formEntity = + FormEntity(EntityAction.CREATE, "things", "id", "", emptyList()) + val formEntities = EntitiesExtra(listOf(formEntity)) + + LocalEntityUseCases.updateLocalEntitiesFromForm( + formEntities, + entitiesRepository + ) + + verify(analytics).logEventWithParam( + AnalyticsEvents.ENTITY_FAILURE, + "failure_code", + "CREATE_NO_LABEL" + ) + } + @Test fun `#updateLocalEntitiesFromServer saves entity from server`() { val csv = createEntityList( diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt index e2e97252303..e4fd65b0b72 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt @@ -21,6 +21,10 @@ import org.javarosa.xform.util.XFormUtils import org.junit.After import org.junit.Before import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.odk.collect.analytics.Analytics +import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor import org.odk.collect.entities.javarosa.finalization.FormEntity @@ -266,4 +270,87 @@ class EntityFormFinalizationProcessorTest { equalTo(Pair("name", "John")) ) } + + @Test + fun `when id is missing, logs NO_ID to analytics`() { + val analytics = mock() + Analytics.setInstance(analytics) + + val scenario = Scenario.init( + "Create entity form", + html( + listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), + head( + title("Create entity form"), + model( + listOf(Pair("entities:entities-version", "2024.1.0")), + mainInstance( + t( + "data id=\"create-entity-form\"", + t("name"), + t("meta", entityNode("people", CREATE)) + ) + ), + bind("/data/name").type("string"), + bind("/data/meta/entity/@id").type("string"), + bind("/data/meta/entity/label").type("string").calculate("/data/name") + ) + ), + body( + input("/data/name") + ) + ) + ) + + val processor = EntityFormFinalizationProcessor() + processor.processForm(scenario.formEntryController.model) + + verify(analytics).logEventWithParam( + AnalyticsEvents.ENTITY_FAILURE, + "failure_code", + "NO_ID" + ) + } + + @Test + fun `when id is invalid, logs INVALID_ID to analytics`() { + val analytics = mock() + Analytics.setInstance(analytics) + + val scenario = Scenario.init( + "Create entity form", + html( + listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), + head( + title("Create entity form"), + model( + listOf(Pair("entities:entities-version", "2024.1.0")), + mainInstance( + t( + "data id=\"create-entity-form\"", + t("name"), + t("meta", entityNode("people", CREATE)) + ) + ), + bind("/data/name").type("string"), + bind("/data/meta/entity/@id").type("string"), + bind("/data/meta/entity/label").type("string").calculate("/data/name"), + setvalue("odk-instance-first-load", "/data/meta/entity/@id", "now()") + ) + ), + body( + input("/data/name") + ) + ) + ) + + val processor = EntityFormFinalizationProcessor() + processor.processForm(scenario.formEntryController.model) + + verify(analytics).logEventWithParam( + AnalyticsEvents.ENTITY_FAILURE, + "failure_code", + "INVALID_ID" + ) + } } From ee1d86ef1cfe1212a12897644ecdc0872f4e179c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 28 May 2026 11:49:08 +0200 Subject: [PATCH 2/7] Remove analytics tests --- .../entities/LocalEntityUseCasesTest.kt | 44 ---------- .../EntityFormFinalizationProcessorTest.kt | 87 ------------------- 2 files changed, 131 deletions(-) diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index 02d6b1edf13..b35af6bd5d8 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -10,8 +10,6 @@ import org.hamcrest.text.IsBlankString.blankOrNullString import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.odk.collect.analytics.Analytics -import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity import org.odk.collect.entities.javarosa.finalization.InvalidEntity @@ -248,48 +246,6 @@ class LocalEntityUseCasesTest { ) } - @Test - fun `#updateLocalEntitiesFromForm logs UPDATE_NO_MATCH when existing entity not found on update`() { - val analytics = mock() - Analytics.setInstance(analytics) - - val formEntity = - FormEntity(EntityAction.UPDATE, "things", "id", "label", emptyList()) - val formEntities = EntitiesExtra(listOf(formEntity)) - - LocalEntityUseCases.updateLocalEntitiesFromForm( - formEntities, - entitiesRepository - ) - - verify(analytics).logEventWithParam( - AnalyticsEvents.ENTITY_FAILURE, - "failure_code", - "UPDATE_NO_MATCH" - ) - } - - @Test - fun `#updateLocalEntitiesFromForm logs CREATE_NO_LABEL when label is blank on create`() { - val analytics = mock() - Analytics.setInstance(analytics) - - val formEntity = - FormEntity(EntityAction.CREATE, "things", "id", "", emptyList()) - val formEntities = EntitiesExtra(listOf(formEntity)) - - LocalEntityUseCases.updateLocalEntitiesFromForm( - formEntities, - entitiesRepository - ) - - verify(analytics).logEventWithParam( - AnalyticsEvents.ENTITY_FAILURE, - "failure_code", - "CREATE_NO_LABEL" - ) - } - @Test fun `#updateLocalEntitiesFromServer saves entity from server`() { val csv = createEntityList( diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt index e4fd65b0b72..e2e97252303 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt @@ -21,10 +21,6 @@ import org.javarosa.xform.util.XFormUtils import org.junit.After import org.junit.Before import org.junit.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.odk.collect.analytics.Analytics -import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor import org.odk.collect.entities.javarosa.finalization.FormEntity @@ -270,87 +266,4 @@ class EntityFormFinalizationProcessorTest { equalTo(Pair("name", "John")) ) } - - @Test - fun `when id is missing, logs NO_ID to analytics`() { - val analytics = mock() - Analytics.setInstance(analytics) - - val scenario = Scenario.init( - "Create entity form", - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Create entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"create-entity-form\"", - t("name"), - t("meta", entityNode("people", CREATE)) - ) - ), - bind("/data/name").type("string"), - bind("/data/meta/entity/@id").type("string"), - bind("/data/meta/entity/label").type("string").calculate("/data/name") - ) - ), - body( - input("/data/name") - ) - ) - ) - - val processor = EntityFormFinalizationProcessor() - processor.processForm(scenario.formEntryController.model) - - verify(analytics).logEventWithParam( - AnalyticsEvents.ENTITY_FAILURE, - "failure_code", - "NO_ID" - ) - } - - @Test - fun `when id is invalid, logs INVALID_ID to analytics`() { - val analytics = mock() - Analytics.setInstance(analytics) - - val scenario = Scenario.init( - "Create entity form", - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Create entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"create-entity-form\"", - t("name"), - t("meta", entityNode("people", CREATE)) - ) - ), - bind("/data/name").type("string"), - bind("/data/meta/entity/@id").type("string"), - bind("/data/meta/entity/label").type("string").calculate("/data/name"), - setvalue("odk-instance-first-load", "/data/meta/entity/@id", "now()") - ) - ), - body( - input("/data/name") - ) - ) - ) - - val processor = EntityFormFinalizationProcessor() - processor.processForm(scenario.formEntryController.model) - - verify(analytics).logEventWithParam( - AnalyticsEvents.ENTITY_FAILURE, - "failure_code", - "INVALID_ID" - ) - } } From f9611f3310a0ba9a3930ad9dcb75e4d61b772f20 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 28 May 2026 12:09:59 +0200 Subject: [PATCH 3/7] Use individual analytics events for failures and track form --- .../collect/entities/LocalEntityUseCases.kt | 5 ++--- .../entities/analytics/AnalyticsEvents.kt | 20 ++++++++++++++++++- .../EntityFormFinalizationProcessor.kt | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index bf65ebe9aa7..010d95e92bf 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -34,7 +34,7 @@ object LocalEntityUseCases { if (existing != null) { saveUpdatedEntity(formEntity, existing, entitiesRepository) } else { - Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "UPDATE_NO_MATCH") + Analytics.log(AnalyticsEvents.ENTITY_UPDATE_NO_MATCH, "form") } } @@ -81,8 +81,7 @@ object LocalEntityUseCases { "Entities", "Failed to create dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" ) - - Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "CREATE_NO_LABEL") + Analytics.log(AnalyticsEvents.ENTITY_CREATE_NO_LABEL, "form") } } diff --git a/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt index a1435cf7d88..4588ab091e6 100644 --- a/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt +++ b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt @@ -1,5 +1,23 @@ package org.odk.collect.entities.analytics object AnalyticsEvents { - const val ENTITY_FAILURE = "EntityFailure" + /** + * Tracks how often an entity update is attempted but no entity with a matching ID is found. + */ + const val ENTITY_UPDATE_NO_MATCH = "EntityUpdateNoMatch" + + /** + * Tracks how often an entity creation is attempted but the label is blank. + */ + const val ENTITY_CREATE_NO_LABEL = "EntityCreateNoLabel" + + /** + * Tracks how often an entity is defined in a form but has no ID. + */ + const val ENTITY_WITH_NO_ID = "EntityWithNoId" + + /** + * Tracks how often an entity is defined in a form but has an invalid ID (not a V4 UUID). + */ + const val INVALID_ENTITY = "InvalidEntity" } diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt index 2020f13043b..c9c1f1224e5 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt @@ -85,9 +85,9 @@ class EntityFormFinalizationProcessor : FormEntryFinalizationProcessor { FormEntity(action, dataset, id, label, fields) } else { if (id.isNullOrBlank()) { - Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "NO_ID") + Analytics.log(AnalyticsEvents.ENTITY_WITH_NO_ID, "form") } else { - Analytics.log(AnalyticsEvents.ENTITY_FAILURE, "failure_code", "INVALID_ID") + Analytics.log(AnalyticsEvents.INVALID_ENTITY, "form") } null } From d060b27f86962bb5719340fa78da2d69e8c4bba3 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 29 May 2026 14:35:03 +0200 Subject: [PATCH 4/7] Move logging invalid entities to one place --- .../collect/entities/LocalEntityUseCases.kt | 58 +++++++++++-------- .../javarosa/finalization/EntitiesExtra.kt | 1 - .../EntityFormFinalizationProcessor.kt | 23 +------- .../javarosa/finalization/FormEntity.kt | 2 +- .../javarosa/finalization/InvalidEntity.kt | 7 --- 5 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 entities/src/main/java/org/odk/collect/entities/javarosa/finalization/InvalidEntity.kt diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index 010d95e92bf..f7882a4a58f 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -7,6 +7,7 @@ import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity import org.odk.collect.entities.javarosa.parse.EntitySchema +import org.odk.collect.entities.javarosa.parse.isV4UUID import org.odk.collect.entities.javarosa.spec.EntityAction import org.odk.collect.entities.server.EntitySource import org.odk.collect.entities.storage.EntitiesRepository @@ -26,35 +27,44 @@ object LocalEntityUseCases { debugLogger: DebugLogger? = null ) { formEntities?.entities?.forEach { formEntity -> - when (formEntity.action) { - EntityAction.CREATE -> saveNewEntity(formEntity, entitiesRepository, debugLogger) - - EntityAction.UPDATE -> { - val existing = entitiesRepository.findEntityById(formEntity.dataset, formEntity.id) - if (existing != null) { - saveUpdatedEntity(formEntity, existing, entitiesRepository) - } else { - Analytics.log(AnalyticsEvents.ENTITY_UPDATE_NO_MATCH, "form") + if (formEntity.id.isV4UUID()) { + when (formEntity.action) { + EntityAction.CREATE -> saveNewEntity(formEntity, entitiesRepository, debugLogger) + + EntityAction.UPDATE -> { + val existing = entitiesRepository.findEntityById(formEntity.dataset, formEntity.id) + if (existing != null) { + saveUpdatedEntity(formEntity, existing, entitiesRepository) + } else { + debugLogger?.log( + "Entities", + "Failed to create update=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" + ) + Analytics.log(AnalyticsEvents.ENTITY_UPDATE_NO_MATCH, "form") + } } - } - EntityAction.UPSERT -> { - val existing = entitiesRepository.findEntityById(formEntity.dataset, formEntity.id) - if (existing == null) { - saveNewEntity(formEntity, entitiesRepository, debugLogger) - } else { - saveUpdatedEntity(formEntity, existing, entitiesRepository) + EntityAction.UPSERT -> { + val existing = entitiesRepository.findEntityById(formEntity.dataset, formEntity.id) + if (existing == null) { + saveNewEntity(formEntity, entitiesRepository, debugLogger) + } else { + saveUpdatedEntity(formEntity, existing, entitiesRepository) + } } } + } else { + debugLogger?.log( + "Entities", + "Failed to create/update dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" + ) + if (formEntity.id.isNullOrBlank()) { + Analytics.log(AnalyticsEvents.ENTITY_WITH_NO_ID, "form") + } else { + Analytics.log(AnalyticsEvents.INVALID_ENTITY, "form") + } } } - - formEntities?.invalidEntities?.forEach { - debugLogger?.log( - "Entities", - "Failed to create/update dataset=${it.dataset}, id=${it.id}, label=${it.label}" - ) - } } private fun saveNewEntity( @@ -68,7 +78,7 @@ object LocalEntityUseCases { entitiesRepository.save( formEntity.dataset, Entity.New( - formEntity.id, + formEntity.id!!, formEntity.label, 1, formEntity.properties, diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt index c0fc4ea6d03..eff96b4db15 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt @@ -2,5 +2,4 @@ package org.odk.collect.entities.javarosa.finalization data class EntitiesExtra( val entities: List = emptyList(), - val invalidEntities: List = emptyList() ) diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt index c9c1f1224e5..5bf9c5200eb 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt @@ -4,11 +4,8 @@ import org.javarosa.core.model.instance.FormInstance import org.javarosa.core.model.instance.TreeReference import org.javarosa.form.api.FormEntryFinalizationProcessor import org.javarosa.form.api.FormEntryModel -import org.odk.collect.analytics.Analytics -import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.parse.EntityFormExtra import org.odk.collect.entities.javarosa.parse.SaveTo -import org.odk.collect.entities.javarosa.parse.isV4UUID import org.odk.collect.entities.javarosa.spec.EntityAction import org.odk.collect.entities.javarosa.spec.EntityFormParser @@ -39,12 +36,7 @@ class EntityFormFinalizationProcessor : FormEntryFinalizationProcessor { mainInstance ) - if (entity != null) { - extra.copy(entities = extra.entities + entity) - } else { - val invalidEntity = InvalidEntity(dataset, id, label) - extra.copy(invalidEntities = extra.invalidEntities + invalidEntity) - } + extra.copy(entities = extra.entities + entity) } else { extra } @@ -62,7 +54,7 @@ class EntityFormFinalizationProcessor : FormEntryFinalizationProcessor { saveTos: List, action: EntityAction, mainInstance: FormInstance - ): FormEntity? { + ): FormEntity { val entityGroupRef = elementRef.getParentRef().getParentRef() val fields = saveTos.mapNotNull { saveTo -> if (!entityGroupRef.genericize().equals(saveTo.entityGroupReference)) { @@ -81,15 +73,6 @@ class EntityFormFinalizationProcessor : FormEntryFinalizationProcessor { } } - return if (id.isV4UUID()) { - FormEntity(action, dataset, id, label, fields) - } else { - if (id.isNullOrBlank()) { - Analytics.log(AnalyticsEvents.ENTITY_WITH_NO_ID, "form") - } else { - Analytics.log(AnalyticsEvents.INVALID_ENTITY, "form") - } - null - } + return FormEntity(action, dataset, id, label, fields) } } diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/FormEntity.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/FormEntity.kt index fc9aef9ba40..5538ed3f4af 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/FormEntity.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/FormEntity.kt @@ -5,7 +5,7 @@ import org.odk.collect.entities.javarosa.spec.EntityAction data class FormEntity( @JvmField val action: EntityAction, @JvmField val dataset: String, - @JvmField val id: String, + @JvmField val id: String?, @JvmField val label: String, @JvmField val properties: List> ) diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/InvalidEntity.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/InvalidEntity.kt deleted file mode 100644 index 2ab7356a371..00000000000 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/InvalidEntity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.odk.collect.entities.javarosa.finalization - -data class InvalidEntity( - val dataset: String, - val id: String?, - val label: String? -) From 00d1f1d76ef14b59f29757195601a7eb53d22d15 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 30 May 2026 22:17:11 +0200 Subject: [PATCH 5/7] Unify entity logging and analytics --- .../android/projects/FileDebugLogger.kt | 10 ++++++ .../collect/entities/LocalEntityUseCases.kt | 35 ++++++++++--------- .../org/odk/collect/shared/DebugLogger.kt | 2 ++ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/projects/FileDebugLogger.kt b/collect_app/src/main/java/org/odk/collect/android/projects/FileDebugLogger.kt index a74a802edf8..fed448416a7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/projects/FileDebugLogger.kt +++ b/collect_app/src/main/java/org/odk/collect/android/projects/FileDebugLogger.kt @@ -1,5 +1,6 @@ package org.odk.collect.android.projects +import org.odk.collect.analytics.Analytics import org.odk.collect.android.BuildConfig import org.odk.collect.shared.DebugLogger import java.io.File @@ -8,6 +9,15 @@ import java.time.LocalDateTime class FileDebugLogger(private val file: File) : DebugLogger { override fun log(tag: String, message: String) { + logToFile(tag, message) + } + + override fun logWithAnalytics(tag: String, message: String, analyticsEvent: String, analyticsKey: String) { + logToFile(tag, message) + Analytics.log(analyticsEvent, analyticsKey) + } + + private fun logToFile(tag: String, message: String) { if (enabled) { val line = "${LocalDateTime.now()} $tag \"$message\"\n" file.appendText(line) diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index f7882a4a58f..91e3541b7a1 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -36,11 +36,7 @@ object LocalEntityUseCases { if (existing != null) { saveUpdatedEntity(formEntity, existing, entitiesRepository) } else { - debugLogger?.log( - "Entities", - "Failed to create update=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" - ) - Analytics.log(AnalyticsEvents.ENTITY_UPDATE_NO_MATCH, "form") + debugLogger?.logInvalidEntity(AnalyticsEvents.ENTITY_UPDATE_NO_MATCH, formEntity) } } @@ -54,15 +50,13 @@ object LocalEntityUseCases { } } } else { - debugLogger?.log( - "Entities", - "Failed to create/update dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" - ) - if (formEntity.id.isNullOrBlank()) { - Analytics.log(AnalyticsEvents.ENTITY_WITH_NO_ID, "form") + val event = if (formEntity.id.isNullOrBlank()) { + AnalyticsEvents.ENTITY_WITH_NO_ID } else { - Analytics.log(AnalyticsEvents.INVALID_ENTITY, "form") + AnalyticsEvents.INVALID_ENTITY } + + debugLogger?.logInvalidEntity(event, formEntity) } } } @@ -87,11 +81,7 @@ object LocalEntityUseCases { ) } } else { - debugLogger?.log( - "Entities", - "Failed to create dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" - ) - Analytics.log(AnalyticsEvents.ENTITY_CREATE_NO_LABEL, "form") + debugLogger?.logInvalidEntity(AnalyticsEvents.ENTITY_CREATE_NO_LABEL, formEntity) } } @@ -243,3 +233,14 @@ private fun Map.removeReservedProperties(): Map { it.key == EntitySchema.ID || it.key == EntitySchema.LABEL || it.key.startsWith("__") } } + +private fun DebugLogger.logInvalidEntity(event: String, formEntity: FormEntity) { + val action = when (event) { + AnalyticsEvents.ENTITY_CREATE_NO_LABEL -> "create" + AnalyticsEvents.ENTITY_UPDATE_NO_MATCH -> "update" + else -> "create/update" + } + val message = "Failed to $action dataset=${formEntity.dataset}, id=${formEntity.id}, label=${formEntity.label}" + + this.logWithAnalytics("Entities", message, event, "form") +} diff --git a/shared/src/main/java/org/odk/collect/shared/DebugLogger.kt b/shared/src/main/java/org/odk/collect/shared/DebugLogger.kt index 45a1643fd5d..c9b24104705 100644 --- a/shared/src/main/java/org/odk/collect/shared/DebugLogger.kt +++ b/shared/src/main/java/org/odk/collect/shared/DebugLogger.kt @@ -2,4 +2,6 @@ package org.odk.collect.shared interface DebugLogger { fun log(tag: String, message: String) + + fun logWithAnalytics(tag: String, message: String, analyticsEvent: String, analyticsKey: String) } From 2dac057ce2bbaeeb1aaf403009476cd3d3ee155d Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 30 May 2026 22:38:21 +0200 Subject: [PATCH 6/7] Fix event name --- .../main/java/org/odk/collect/entities/LocalEntityUseCases.kt | 3 +-- .../java/org/odk/collect/entities/analytics/AnalyticsEvents.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index 91e3541b7a1..2ca23b6ebd9 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -2,7 +2,6 @@ package org.odk.collect.entities import org.apache.commons.csv.CSVRecord import org.javarosa.core.model.instance.SecondaryInstanceCSVParserBuilder -import org.odk.collect.analytics.Analytics import org.odk.collect.entities.analytics.AnalyticsEvents import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity @@ -53,7 +52,7 @@ object LocalEntityUseCases { val event = if (formEntity.id.isNullOrBlank()) { AnalyticsEvents.ENTITY_WITH_NO_ID } else { - AnalyticsEvents.INVALID_ENTITY + AnalyticsEvents.ENTITY_WITH_INVALID_ID } debugLogger?.logInvalidEntity(event, formEntity) diff --git a/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt index 4588ab091e6..814d9d32dc6 100644 --- a/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt +++ b/entities/src/main/java/org/odk/collect/entities/analytics/AnalyticsEvents.kt @@ -19,5 +19,5 @@ object AnalyticsEvents { /** * Tracks how often an entity is defined in a form but has an invalid ID (not a V4 UUID). */ - const val INVALID_ENTITY = "InvalidEntity" + const val ENTITY_WITH_INVALID_ID = "EntityWithInvalidId" } From e99abe2bd57171784dcc00eb50e43dca4522dde5 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Sat, 30 May 2026 23:21:26 +0200 Subject: [PATCH 7/7] Update tests --- .../entities/LocalEntityUseCasesTest.kt | 99 ++++++++--- .../collect/entities/javarosa/EntitiesTest.kt | 158 ------------------ 2 files changed, 80 insertions(+), 177 deletions(-) diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt index b35af6bd5d8..9aef0c838b3 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt @@ -12,7 +12,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import org.odk.collect.entities.javarosa.finalization.FormEntity -import org.odk.collect.entities.javarosa.finalization.InvalidEntity import org.odk.collect.entities.javarosa.parse.EntitySchema import org.odk.collect.entities.javarosa.spec.EntityAction import org.odk.collect.entities.server.EntitySource @@ -37,7 +36,7 @@ class LocalEntityUseCasesTest { entitiesRepository.addList("things") val formEntity = - FormEntity(EntityAction.CREATE, "things", "id", "label", listOf("property" to "value")) + FormEntity(EntityAction.CREATE, "things", UUID.randomUUID().toString(), "label", listOf("property" to "value")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -49,12 +48,25 @@ class LocalEntityUseCasesTest { assertThat(entities[0].branchId, not(blankOrNullString())) } + @Test + fun `#updateLocalEntitiesFromForm does not save a new entity on create if id is not a valid UUID`() { + entitiesRepository.addList("things") + + val formEntity = + FormEntity(EntityAction.CREATE, "things", "id", "label", listOf("property" to "value")) + val formEntities = EntitiesExtra(listOf(formEntity)) + LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) + + val entities = entitiesRepository.query("things") + assertThat(entities.size, equalTo(0)) + } + @Test fun `#updateLocalEntitiesFromForm does not save a new entity on create if label is blank`() { entitiesRepository.addList("things") val formEntity = - FormEntity(EntityAction.CREATE, "things", "id", "", listOf("property" to "value")) + FormEntity(EntityAction.CREATE, "things", UUID.randomUUID().toString(), "", listOf("property" to "value")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -65,7 +77,7 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm does not save a new entity on create if the list doesn't already exist`() { val formEntity = - FormEntity(EntityAction.CREATE, "things", "id", "label", listOf("property" to "value")) + FormEntity(EntityAction.CREATE, "things", UUID.randomUUID().toString(), "label", listOf("property" to "value")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -74,7 +86,7 @@ class LocalEntityUseCasesTest { } @Test - fun `#updateLocalEntitiesFromForm increments version on update`() { + fun `#updateLocalEntitiesFromForm does not update an entity if id in not a valid UUID`() { entitiesRepository.save( "things", Entity.New( @@ -85,7 +97,30 @@ class LocalEntityUseCasesTest { ) val formEntity = - FormEntity(EntityAction.UPDATE, "things", "id", "label", emptyList()) + FormEntity(EntityAction.UPDATE, "things", "id", "new_label", emptyList()) + val formEntities = EntitiesExtra(listOf(formEntity)) + + LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) + val entities = entitiesRepository.query("things") + assertThat(entities.size, equalTo(1)) + assertThat(entities[0].label, equalTo("label")) + assertThat(entities[0].version, equalTo(1)) + } + + @Test + fun `#updateLocalEntitiesFromForm increments version on update`() { + val id = UUID.randomUUID().toString() + entitiesRepository.save( + "things", + Entity.New( + id, + "label", + version = 1 + ) + ) + + val formEntity = + FormEntity(EntityAction.UPDATE, "things", id, "label", emptyList()) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -96,10 +131,11 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm updates properties on update`() { + val id = UUID.randomUUID().toString() entitiesRepository.save( "things", Entity.New( - "id", + id, "label", version = 1, properties = listOf("prop" to "value") @@ -107,7 +143,7 @@ class LocalEntityUseCasesTest { ) val formEntity = - FormEntity(EntityAction.UPDATE, "things", "id", "label", listOf("prop" to "value 2")) + FormEntity(EntityAction.UPDATE, "things", id, "label", listOf("prop" to "value 2")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -119,10 +155,11 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm updates properties and does not change label on update if label is blank`() { + val id = UUID.randomUUID().toString() entitiesRepository.save( "things", Entity.New( - "id", + id, "label", version = 1, properties = listOf("prop" to "value") @@ -130,7 +167,7 @@ class LocalEntityUseCasesTest { ) val formEntity = - FormEntity(EntityAction.UPDATE, "things", "id", " ", listOf("prop" to "value 2")) + FormEntity(EntityAction.UPDATE, "things", id, " ", listOf("prop" to "value 2")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -146,7 +183,7 @@ class LocalEntityUseCasesTest { entitiesRepository.addList("things") val formEntity = - FormEntity(EntityAction.UPSERT, "things", "id", "label", listOf("property" to "value")) + FormEntity(EntityAction.UPSERT, "things", UUID.randomUUID().toString(), "label", listOf("property" to "value")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -163,7 +200,7 @@ class LocalEntityUseCasesTest { entitiesRepository.addList("things") val formEntity = - FormEntity(EntityAction.UPSERT, "things", "id", "", listOf("property" to "value")) + FormEntity(EntityAction.UPSERT, "things", UUID.randomUUID().toString(), "", listOf("property" to "value")) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -173,17 +210,18 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm updates an existing entity on upsert if it exists`() { + val id = UUID.randomUUID().toString() entitiesRepository.save( "things", Entity.New( - "id", + id, "label", version = 1 ) ) val formEntity = - FormEntity(EntityAction.UPSERT, "things", "id", "new label", emptyList()) + FormEntity(EntityAction.UPSERT, "things", id, "new label", emptyList()) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -207,7 +245,7 @@ class LocalEntityUseCasesTest { ) val formEntity = - FormEntity(EntityAction.UPDATE, "things", "id", "label", emptyList()) + FormEntity(EntityAction.UPDATE, "things", UUID.randomUUID().toString(), "label", emptyList()) val formEntities = EntitiesExtra(listOf(formEntity)) LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository) @@ -220,7 +258,7 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm does not save updated entity that doesn't already exist`() { val formEntity = - FormEntity(EntityAction.UPDATE, "things", "1", "1", emptyList()) + FormEntity(EntityAction.UPDATE, "things", UUID.randomUUID().toString(), "1", emptyList()) val formEntities = EntitiesExtra(listOf(formEntity)) entitiesRepository.addList("things") @@ -231,8 +269,17 @@ class LocalEntityUseCasesTest { @Test fun `#updateLocalEntitiesFromForm logs invalid entities`() { val debugLogger = mock() + val id = UUID.randomUUID().toString() + val formEntity1 = + FormEntity(EntityAction.CREATE, "things", "", "label", emptyList()) + val formEntity2 = + FormEntity(EntityAction.CREATE, "things", "id", "label", emptyList()) + val formEntity3 = + FormEntity(EntityAction.CREATE, "things", id, "", emptyList()) + val formEntity4 = + FormEntity(EntityAction.UPDATE, "things", id, "", emptyList()) val formEntities = - EntitiesExtra(emptyList(), listOf(InvalidEntity("things", "id", "label"))) + EntitiesExtra(listOf(formEntity1, formEntity2, formEntity3, formEntity4)) LocalEntityUseCases.updateLocalEntitiesFromForm( formEntities, @@ -240,9 +287,23 @@ class LocalEntityUseCasesTest { debugLogger ) - verify(debugLogger).log( + verify(debugLogger).logWithAnalytics( + "Entities", + "Failed to create/update dataset=things, id=, label=label", + "EntityWithNoId", + "form" + ) + verify(debugLogger).logWithAnalytics( + "Entities", + "Failed to create/update dataset=things, id=id, label=label", + "EntityWithInvalidId", + "form" + ) + verify(debugLogger).logWithAnalytics( "Entities", - "Failed to create/update dataset=things, id=id, label=label" + "Failed to update dataset=things, id=$id, label=", + "EntityUpdateNoMatch", + "form" ) } diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.kt index fe6ba668821..d2287358353 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.kt @@ -678,128 +678,6 @@ class EntitiesTest { ) } - @Test - fun `filling form with create without an id makes invalid entity available`() { - val scenario = Scenario.init( - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Create entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"create-entity-form\"", - t("id"), - t("name"), - t( - "meta", - t("entity dataset=\"people\" create=\"1\" id=\"\"", - t("label") - ) - ) - ) - ), - bind("/data/id").type("string"), - bind("/data/meta/entity/@id").type("string").calculate("/data/id"), - bind("/data/meta/entity/label").type("string").calculate("/data/name") - ) - ), - body( - input("/data/id"), - input("/data/name") - ) - ) - ) - - scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor()) - scenario.finalizeInstance() - - val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java) - val (entities, invalidEntities) = entitiesExtra - assertThat(entities.size, equalTo(0)) - assertThat(invalidEntities.size, equalTo(1)) - assertThat(invalidEntities[0].dataset, equalTo("people")) - assertThat(invalidEntities[0].id, equalTo(null)) - } - - @Test - fun `filling form with blank label makes invalid entity available`() { - val scenario = Scenario.init( - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Create entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"create-entity-form\"", - t("name"), - t("meta", entityNode("people", CREATE)) - ) - ), - bind("/data/name").type("string").withSaveTo("name"), - entityLabelBind("/data/name"), - ) - ), - body( - input("/data/name") - ) - ) - ) - - scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor()) - scenario.answer("/data/name", " ") - scenario.finalizeInstance() - - val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java) - val (entities, invalidEntities) = entitiesExtra - assertThat(entities.size, equalTo(0)) - assertThat(invalidEntities.size, equalTo(1)) - assertThat(invalidEntities[0].dataset, equalTo("people")) - assertThat(invalidEntities[0].label, equalTo(" ")) - } - - @Test - fun `filling fom with non-UUID id makes invalid entity available`() { - val scenario = Scenario.init( - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Create entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"create-entity-form\"", - t("name"), - t("meta", entityNode("people", CREATE)) - ) - ), - bind("/data/name").type("string").withSaveTo("name"), - entityLabelBind("/data/name"), - setvalue("odk-instance-first-load", "/data/meta/entity/@id", "1") - ) - ), - body( - input("/data/name") - ) - ) - ) - - scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor()) - scenario.answer("/data/name", "Dylan") - scenario.finalizeInstance() - - val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java) - val (entities, invalidEntities) = entitiesExtra - assertThat(entities.size, equalTo(0)) - assertThat(invalidEntities.size, equalTo(1)) - assertThat(invalidEntities[0].dataset, equalTo("people")) - assertThat(invalidEntities[0].id, equalTo("1")) - } - @Test fun `filling form with update makes entity available`() { val scenario = Scenario.init( @@ -846,42 +724,6 @@ class EntitiesTest { assertThat(entities[0].action, equalTo(EntityAction.UPDATE)) } - @Test - fun `filling form with update without an id does not make entity available`() { - val scenario = Scenario.init( - html( - listOf(Pair("entities", "http://www.opendatakit.org/xforms/entities")), - head( - title("Update entity form"), - model( - listOf(Pair("entities:entities-version", "2024.1.0")), - mainInstance( - t( - "data id=\"update-entity-form\"", - t("id"), - t( - "meta", - t("entity dataset=\"people\" update=\"1\" id=\"\" baseVersion=\"\"") - ) - ) - ), - bind("/data/id").type("string"), - bind("/data/meta/entity/@id").type("string").calculate("/data/id").readonly() - ) - ), - body( - input("/data/id") - ) - ) - ) - - scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor()) - scenario.finalizeInstance() - - val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities - assertThat(entities.size, equalTo(0)) - } - @Test fun `filling form with create and update makes entity available with upsert action`() { val scenario = Scenario.init(