diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cf3c86c..c731dbf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,89 @@
# Changelog
+## [2.4.0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.4.0) (2022-10-02, release)
+
+### 🚀 Nouveautés
+
+* Refonte ergonomique des listes de choix des nomenclatures. Cette refonte ne concerne pour l'instant
+ que l'étape "Informations" lors de la saisie d'un taxon.
+* Accélérer la saisie en permettant de mémoriser les dernières nomenclatures saisies (https://github.com/PnX-SI/gn_mobile_occtax/issues/169).
+ Cette fonctionnalité est accessible via la propriété `nomenclature/save_default_values` dans le
+ [fichier de paramétrage](https://github.com/PnX-SI/gn_mobile_occtax/blob/develop/README.md#nomenclature-settings).
+* Amélioration sur la recherche des taxons, notamment sur la distinction des mots (avec ou sans
+ majuscules, avec ou sans accents) (https://github.com/PnX-SI/gn_mobile_occtax/issues/91).
+* Petites améliorations sur la présentation des jeux de données, aussi bien dans la page de
+ sélection des jeux de données que dans l'affichage du jeu de données sélectionnée dans la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/120).
+* Petites améliorations sur la page de sélection des observateurs et sur la fonction de recherche
+ des observateurs (https://github.com/PnX-SI/gn_mobile_occtax/issues/142).
+* Petites améliorations sur les messages d'information lors de la synchronisation des données (https://github.com/PnX-SI/gn_mobile_occtax/issues/143).
+* Affichage du nom vernaculaire du taxon dans le bilan de la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/153).
+* Ajout d'une fonction de filtre sur les rangs taxonomique des taxons dans la page du bilan de la
+ saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/166).
+* Affichage du nombre de taxon en en-tête de page (https://github.com/PnX-SI/gn_mobile_occtax/issues/167).
+* Permettre de modifier la date et l'heure de fin des relevés en fin de saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/168).
+* Refonte ergonomique sur l'enchaînement des écrans de la saisie. Le bilan de la saisie intervient
+ notamment après le pointage sur la carte si le relevé contient au moins un taxon (https://github.com/PnX-SI/gn_mobile_occtax/issues/177).
+
+### 🐛 Corrections
+
+* Défilement automatique du nom vernaculaire du taxon sélectionné (https://github.com/PnX-SI/gn_mobile_occtax/issues/49).
+* Validation sur l'ensemble des taxons ajoutés au relevé (https://github.com/PnX-SI/gn_mobile_occtax/issues/177).
+* Correction concernant la mémorisation de la sélection des observateurs lors de la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/110).
+* Validation automatique du compte utilisateur lors de l'authentification (https://github.com/PnX-SI/gn_mobile_occtax/issues/184).
+
+### ⚠️ Notes de version
+
+* Code de version : 3090
+
+## [2.4.0-rc2](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.4.0-rc2) (2022-09-26, pre-release)
+
+### 🚀 Nouveautés
+
+* Refonte ergonomique des listes de choix des nomenclatures.
+* Accélérer la saisie en permettant de mémoriser les dernières nomenclatures saisies (https://github.com/PnX-SI/gn_mobile_occtax/issues/169).
+
+### ⚠️ Notes de version
+
+* Code de version : 3083
+
+## [2.4.0-rc1](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.4.0-rc1) (2022-09-10, pre-release)
+
+### 🐛 Corrections
+
+* Défilement automatique du nom vernaculaire du taxon sélectionné (https://github.com/PnX-SI/gn_mobile_occtax/issues/49).
+* Validation sur l'ensemble des taxons ajoutés au relevé (https://github.com/PnX-SI/gn_mobile_occtax/issues/177).
+
+### ⚠️ Notes de version
+
+* Code de version : 3079
+
+## [2.4.0-rc0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.4.0-rc0) (2022-09-07, pre-release)
+
+### 🚀 Nouveautés
+
+* Amélioration sur la recherche des taxons, notamment sur la distinction des mots (avec ou sans
+ majuscules, avec ou sans accents) (https://github.com/PnX-SI/gn_mobile_occtax/issues/91).
+* Petites améliorations sur la présentation des jeux de données, aussi bien dans la page de
+ sélection des jeux de données que dans l'affichage du jeu de données sélectionnée dans la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/120).
+* Petites améliorations sur la page de sélection des observateurs et sur la fonction de recherche
+ des observateurs (https://github.com/PnX-SI/gn_mobile_occtax/issues/142).
+* Petites améliorations sur les messages d'information lors de la synchronisation des données (https://github.com/PnX-SI/gn_mobile_occtax/issues/143).
+* Affichage du nom vernaculaire du taxon dans le bilan de la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/153).
+* Ajout d'une fonction de filtre sur les rangs taxonomique des taxons dans la page du bilan de la
+ saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/166).
+* Affichage du nombre de taxon en en-tête de page (https://github.com/PnX-SI/gn_mobile_occtax/issues/167).
+* Permettre de modifier la date et l'heure de fin des relevés en fin de saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/168).
+* Refonte ergonomique sur l'enchaînement des écrans de la saisie. Le bilan de la saisie intervient
+ notamment après le pointage sur la carte si le relevé contient au moins un taxon (https://github.com/PnX-SI/gn_mobile_occtax/issues/177).
+
+### 🐛 Corrections
+
+* Correction concernant la mémorisation de la sélection des observateurs lors de la saisie (https://github.com/PnX-SI/gn_mobile_occtax/issues/110).
+
+### ⚠️ Notes de version
+
+* Code de version : 3075
+
## [2.3.0](https://github.com/PnX-SI/gn_mobile_occtax/releases/tag/2.3.0) (2022-07-14, release)
### 🚀 Nouveautés
diff --git a/README.md b/README.md
index 8d0c08d6..52b667d0 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Based on [datasync module](https://github.com/PnX-SI/gn_mobile_core) to synchron
## Launcher icons
| Name | Flavor | Launcher icon |
-| ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|---------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Default | _generic_ | ![PNX](https://raw.githubusercontent.com/PnX-SI/gn_mobile_occtax/develop/occtax/src/main/res/mipmap-xxxhdpi/ic_launcher.png) ![PNX_debug](https://raw.githubusercontent.com/PnX-SI/gn_mobile_occtax/develop/occtax/src/debug/res/mipmap-xxxhdpi/ic_launcher.png) |
## Settings
@@ -81,16 +81,17 @@ Example:
### Parameters description
-| Parameter | UI | Description | Default value |
-| --------------------------- | ------- | -------------------------------------------------------------------------------------------------- | ------------- |
-| `area_observation_duration` | ☐ | Area observation duration period (in days) | 365 |
-| `sync` | ☐ | Data synchronization settings (cf. https://github.com/PnX-SI/gn_mobile_core/tree/develop/datasync) | |
-| `map` | ☐ | Maps settings (cf. https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps) | |
-| `input` | ☐ | Input form settings | |
-| `input/date` | ☐ | Date settings | |
-| `nomenclature` | ☐ | Nomenclature settings | |
-| `nomenclature/information` | ☐ | Information settings (as array) | |
-| `nomenclature/counting` | ☐ | Counting settings (as array) | |
+| Parameter | UI | Description | Default value |
+|------------------------------------|---------|----------------------------------------------------------------------------------------------------|---------------|
+| `area_observation_duration` | ☐ | Area observation duration period (in days) | 365 |
+| `sync` | ☐ | Data synchronization settings (cf. https://github.com/PnX-SI/gn_mobile_core/tree/develop/datasync) | |
+| `map` | ☐ | Maps settings (cf. https://github.com/PnX-SI/gn_mobile_maps/tree/develop/maps) | |
+| `input` | ☐ | Input form settings | |
+| `input/date` | ☐ | Date settings | |
+| `nomenclature` | ☐ | Nomenclature settings | |
+| `nomenclature/save_default_values` | ☐ | Save default nomenclature values | false |
+| `nomenclature/information` | ☐ | Information settings (as array) | |
+| `nomenclature/counting` | ☐ | Counting settings (as array) | |
### Input settings
@@ -101,7 +102,7 @@ Allows to configure settings related to user input.
How the user can set the start and end date of the input:
| Parameter | Description | Default value |
-| ----------------- | ---------------------------------------------------------------------------- | ------------- |
+|-------------------|------------------------------------------------------------------------------|---------------|
| `enable_end_date` | Whether to edit as well the end date of the input | `false` |
| `enable_hours` | Whether to edit as well the hour part of the start and end date (if enabled) | `false` |
@@ -116,15 +117,18 @@ If nothing is configured, only the start date without the hour part is editable.
### Nomenclature settings
-Allows to define if fields are displayed by default and if they are editable (visible). If a field is not editable (visible),
-it will use the default value set in Occtax database.
+`save_default_values`: Allows to save locally and only during a session of use selected nomenclature
+values as default values (default: `false`).
+
+Allows to define if fields are displayed by default and if they are editable (visible).
+If a field is not editable (visible), it will use the default value set in Occtax database.
All these settings may not be defined and the default values will then be used instead:
**Information settings**
| Nomenclature | Label | Displayed by default | Editable (visible) |
-| ---------------- | -------------------- | -------------------- | ------------------ |
+|------------------|----------------------|----------------------|--------------------|
| METH_OBS | Observation methods | `true` | `true` |
| ETA_BIO | Biological state | `true` | `true` |
| METH_DETERMIN | Determination method | `false` | `true` |
@@ -138,7 +142,7 @@ All these settings may not be defined and the default values will then be used i
**Counting settings**
| Nomenclature | Label | Displayed by default | Editable (visible) |
-| ------------ | -------------------------- | -------------------- | ------------------ |
+|--------------|----------------------------|----------------------|--------------------|
| STADE_VIE | Life stage | `true` | `true` |
| SEXE | Sex | `true` | `true` |
| OBJ_DENBR | Purpose of the enumeration | `true` | `true` |
@@ -153,6 +157,7 @@ You can override these default settings by adding a property for each nomenclatu
```json
{
"nomenclature": {
+ "save_default_values": false,
"information": [
"METH_OBS",
{
@@ -192,7 +197,7 @@ You can override these default settings by adding a property for each nomenclatu
Each property may be a simple string representing the nomenclature attribute to show or an object with the following properties:
| Property | Description | Mandatory |
-| --------- | --------------------------------------------------------------------- | --------- |
+|-----------|-----------------------------------------------------------------------|-----------|
| `key` | The nomenclature attribute | ☑ |
| `visible` | If this attribute is visible (thus editable) or not (default: `true`) | ☐ |
| `default` | If this attribute is shown by default (default: `true`) | ☐ |
@@ -275,4 +280,4 @@ A full build can be executed with the following command:
## Financial support
-This application have been developped with the financial support of the [Office Français de la Biodiversité](https://www.ofb.gouv.fr/)
+This application have been developed with the financial support of the [Office Français de la Biodiversité](https://www.ofb.gouv.fr).
diff --git a/gn_mobile_core b/gn_mobile_core
index cdf89b6d..40f7ddea 160000
--- a/gn_mobile_core
+++ b/gn_mobile_core
@@ -1 +1 @@
-Subproject commit cdf89b6d9894f2e2c9deb6237ae034bae1ec2593
+Subproject commit 40f7ddeab6a8848ca7e3aed7b85298780dbdb88d
diff --git a/gn_mobile_maps b/gn_mobile_maps
index fbae4018..b67e4b3d 160000
--- a/gn_mobile_maps
+++ b/gn_mobile_maps
@@ -1 +1 @@
-Subproject commit fbae40182921bfbfa7e09fd955211fc051f00706
+Subproject commit b67e4b3dbac61878225aec98bda23ec7330a033f
diff --git a/occtax/build.gradle b/occtax/build.gradle
index b752705a..ee06d46d 100644
--- a/occtax/build.gradle
+++ b/occtax/build.gradle
@@ -5,8 +5,6 @@ plugins {
id 'kotlin-kapt'
}
-version = "2.2.0"
-
android {
compileSdkVersion 31
@@ -23,8 +21,8 @@ android {
applicationId "fr.geonature.occtax2"
minSdkVersion 26
targetSdkVersion 31
- versionCode 3070
- versionName "2.3.0"
+ versionCode 3090
+ versionName "2.4.0"
buildConfigField "String", "BUILD_DATE", "\"" + new Date().getTime() + "\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
archivesBaseName = project.name + "-" + versionName
@@ -88,6 +86,11 @@ dependencies {
runtimeOnly "org.tinylog:tinylog-impl:$tinylog_version"
// Testing dependencies
+ testImplementation 'androidx.arch.core:core-testing:2.1.0'
+ testImplementation 'androidx.test.ext:junit-ktx:1.1.3'
+ testImplementation 'io.mockk:mockk:1.12.3'
+ testImplementation 'io.mockk:mockk-agent-jvm:1.12.3'
testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation 'org.robolectric:robolectric:4.8.1'
}
diff --git a/occtax/src/main/AndroidManifest.xml b/occtax/src/main/AndroidManifest.xml
index 77e33c45..8e917ec8 100644
--- a/occtax/src/main/AndroidManifest.xml
+++ b/occtax/src/main/AndroidManifest.xml
@@ -49,6 +49,7 @@
android:theme="@style/AppTheme.NoActionBar" />
+
+ /**
+ * Gets all [Nomenclature] as default nomenclature values.
+ *
+ * @return a list of default [Nomenclature]
+ */
+ suspend fun getAllDefaultNomenclatureValues(): List
+
+ /**
+ * Gets all nomenclature values matching given nomenclature type and an optional taxonomy rank.
+ *
+ * @param mnemonic the nomenclature type as main filter
+ * @param taxonomy the taxonomy rank
+ *
+ * @return a list of [Nomenclature] matching given criteria
+ */
+ suspend fun getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic: String,
+ taxonomy: Taxonomy? = null
+ ): List
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/INomenclatureSettingsLocalDataSource.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/INomenclatureSettingsLocalDataSource.kt
new file mode 100644
index 00000000..1ea0ee37
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/INomenclatureSettingsLocalDataSource.kt
@@ -0,0 +1,24 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.settings.NomenclatureSettings
+import fr.geonature.occtax.settings.PropertySettings
+
+/**
+ * Local data source about nomenclature types settings.
+ *
+ * @author S. Grimault
+ */
+interface INomenclatureSettingsLocalDataSource {
+
+ /**
+ * Gets all [EditableNomenclatureType] matching given nomenclature main type.
+ *
+ * @return a list of [EditableNomenclatureType]
+ */
+ suspend fun getNomenclatureTypeSettings(
+ type: BaseEditableNomenclatureType.Type,
+ vararg defaultPropertySettings: PropertySettings
+ ): List
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/IPropertyValueLocalDataSource.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/IPropertyValueLocalDataSource.kt
new file mode 100644
index 00000000..118f45f9
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/IPropertyValueLocalDataSource.kt
@@ -0,0 +1,57 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * [PropertyValue] local data source.
+ *
+ * @author S. Grimault
+ */
+interface IPropertyValueLocalDataSource {
+
+ /**
+ * Gets all property values matching given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank as filter
+ */
+ suspend fun getPropertyValues(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ )
+ ): List
+
+ /**
+ * Adds or updates given property value for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param propertyValue the property value to add or update
+ */
+ suspend fun setPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ vararg propertyValue: PropertyValue
+ )
+
+ /**
+ * Remove given property value by its code for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param code the property value code to remove
+ */
+ suspend fun clearPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ vararg code: String
+ )
+
+ /**
+ * Clears all saved property values.
+ */
+ suspend fun clearAllPropertyValues()
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImpl.kt
new file mode 100644
index 00000000..157bb76c
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImpl.kt
@@ -0,0 +1,33 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * In memory implementation of [IPropertyValueLocalDataSource].
+ *
+ * @author S. Grimault
+ */
+class InMemoryPropertyValueLocalDataSourceImpl : IPropertyValueLocalDataSource {
+ private val propertyValues = mutableMapOf>()
+
+ override suspend fun getPropertyValues(taxonomy: Taxonomy): List {
+ return propertyValues[taxonomy]?.toList() ?: emptyList()
+ }
+
+ override suspend fun setPropertyValue(taxonomy: Taxonomy, vararg propertyValue: PropertyValue) {
+ propertyValues[taxonomy] =
+ getPropertyValues(taxonomy).filter { existingPropertyValue -> propertyValue.none { it.code == existingPropertyValue.code } }
+ .toSet() + propertyValue.toSet()
+ }
+
+ override suspend fun clearPropertyValue(taxonomy: Taxonomy, vararg code: String) {
+ propertyValues[taxonomy] =
+ getPropertyValues(taxonomy).filter { propertyValue -> code.none { it == propertyValue.code } }
+ .toSet()
+ }
+
+ override suspend fun clearAllPropertyValues() {
+ propertyValues.clear()
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceImpl.kt
new file mode 100644
index 00000000..9c9b263c
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceImpl.kt
@@ -0,0 +1,38 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import fr.geonature.commons.data.dao.NomenclatureDao
+import fr.geonature.commons.data.dao.NomenclatureTypeDao
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.NomenclatureType
+import fr.geonature.commons.data.entity.Taxonomy
+
+/**
+ * Default implementation of [INomenclatureLocalDataSource] using local database.
+ *
+ * @author S. Grimault
+ */
+class NomenclatureLocalDataSourceImpl(
+ private val moduleName: String,
+ private val nomenclatureTypeDao: NomenclatureTypeDao,
+ private val nomenclatureDao: NomenclatureDao
+) : INomenclatureLocalDataSource {
+
+ override suspend fun getAllNomenclatureTypes(): List {
+ return nomenclatureTypeDao.findAll()
+ }
+
+ override suspend fun getAllDefaultNomenclatureValues(): List {
+ return nomenclatureDao.findAllDefaultNomenclatureValues(moduleName)
+ }
+
+ override suspend fun getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic: String,
+ taxonomy: Taxonomy?
+ ): List {
+ return nomenclatureDao.findAllByNomenclatureTypeAndByTaxonomy(
+ mnemonic,
+ taxonomy?.kingdom,
+ taxonomy?.group
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceImpl.kt
new file mode 100644
index 00000000..865a8b8a
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceImpl.kt
@@ -0,0 +1,125 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.settings.PropertySettings
+
+/**
+ * Default implementation of [INomenclatureSettingsLocalDataSource].
+ *
+ * @author S. Grimault
+ */
+class NomenclatureSettingsLocalDataSourceImpl :
+ INomenclatureSettingsLocalDataSource {
+
+ private val defaultNomenclatureTypes = listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.DEFAULT,
+ "TYP_GRP",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_DETERMIN",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "OCC_COMPORTEMENT",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "NATURALITE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "PREUVE_EXIST",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "COMMENT",
+ BaseEditableNomenclatureType.ViewType.TEXT_MULTIPLE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "STADE_VIE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "SEXE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "OBJ_DENBR",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "TYP_DENBR",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "MIN",
+ BaseEditableNomenclatureType.ViewType.MIN_MAX
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "MAX",
+ BaseEditableNomenclatureType.ViewType.MIN_MAX
+ )
+ )
+
+ override suspend fun getNomenclatureTypeSettings(
+ type: BaseEditableNomenclatureType.Type,
+ vararg defaultPropertySettings: PropertySettings
+ ): List {
+ if (defaultPropertySettings.isEmpty()) {
+ return defaultNomenclatureTypes.filter { it.type == type }
+ }
+
+ return defaultPropertySettings
+ .mapNotNull { property ->
+ defaultNomenclatureTypes.find { it.code == property.key }?.let {
+ BaseEditableNomenclatureType.from(
+ it.type,
+ it.code,
+ it.viewType,
+ property.visible,
+ property.default
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureType.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureType.kt
new file mode 100644
index 00000000..f350b2f4
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureType.kt
@@ -0,0 +1,225 @@
+package fr.geonature.occtax.features.nomenclature.domain
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.core.os.ParcelCompat.readBoolean
+import androidx.core.os.ParcelCompat.writeBoolean
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * Definition of an editable nomenclature type.
+ *
+ * @author S. Grimault
+ */
+abstract class BaseEditableNomenclatureType {
+ /**
+ * Main nomenclature type.
+ */
+ abstract val type: Type
+
+ /**
+ * Mnemonic code from nomenclature type.
+ */
+ abstract val code: String
+
+ /**
+ * The corresponding view type.
+ */
+ abstract val viewType: ViewType
+
+ /**
+ * Whether this property is visible (thus editable directly, default: `true`).
+ */
+ abstract val visible: Boolean
+
+ /**
+ * Whether this property is shown by default (default: `true`)
+ */
+ abstract val default: Boolean
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as BaseEditableNomenclatureType
+
+ if (type != other.type) return false
+ if (code != other.code) return false
+ if (viewType != other.viewType) return false
+ if (visible != other.visible) return false
+ if (default != other.default) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = type.hashCode()
+ result = 31 * result + code.hashCode()
+ result = 31 * result + viewType.hashCode()
+ result = 31 * result + visible.hashCode()
+ result = 31 * result + default.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "BaseEditableNomenclatureType(type=$type, code='$code', viewType=$viewType, visible=$visible, default=$default)"
+ }
+
+ companion object {
+
+ /**
+ * Factory to create [BaseEditableNomenclatureType] from given properties.
+ */
+ fun from(
+ type: Type,
+ code: String,
+ viewType: ViewType,
+ visible: Boolean = true,
+ default: Boolean = true
+ ): BaseEditableNomenclatureType = object : BaseEditableNomenclatureType() {
+ override val type: Type
+ get() = type
+ override val code: String
+ get() = code
+ override val viewType: ViewType
+ get() = viewType
+ override val visible: Boolean
+ get() = visible
+ override val default: Boolean
+ get() = default
+ }
+ }
+
+ /**
+ * Describes main editable nomenclature type.
+ */
+ enum class Type {
+ /**
+ * Default nomenclature types.
+ */
+ DEFAULT,
+
+ /**
+ * Nomenclature types used for main information.
+ */
+ INFORMATION,
+
+ /**
+ * Nomenclature types used for describing counting.
+ */
+ COUNTING
+ }
+
+ /**
+ * Describes an editable nomenclature type view type.
+ */
+ enum class ViewType {
+ /**
+ * No specific view type.
+ */
+ NONE,
+
+ /**
+ * As dropdown menu items.
+ */
+ NOMENCLATURE_TYPE,
+
+ /**
+ * As a simple text field.
+ */
+ TEXT_SIMPLE,
+
+ /**
+ * As multi-lines text field.
+ */
+ TEXT_MULTIPLE,
+
+ /**
+ * As a bounded numerical value.
+ */
+ MIN_MAX
+ }
+}
+
+/**
+ * Describes an editable nomenclature type with value.
+ *
+ * @author S. Grimault
+ */
+data class EditableNomenclatureType(
+ override val type: Type,
+ override val code: String,
+ override val viewType: ViewType,
+ override val visible: Boolean = true,
+ override val default: Boolean = true,
+
+ /**
+ * Nomenclature type's label.
+ */
+ val label: String? = null,
+
+ /**
+ * The current value for this nomenclature type.
+ */
+ var value: PropertyValue? = null,
+
+ /**
+ * Whether this property is locked for modification (default: `false`).
+ */
+ var locked: Boolean = false
+) : BaseEditableNomenclatureType(), Parcelable {
+
+ private constructor(source: Parcel) : this(
+ source.readSerializable() as Type,
+ source.readString()!!,
+ source.readSerializable() as ViewType,
+ readBoolean(source),
+ readBoolean(source),
+ source.readString(),
+ source.readParcelable(PropertyValue::class.java.classLoader),
+ readBoolean(source)
+ )
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(
+ dest: Parcel?,
+ flags: Int
+ ) {
+ dest?.also {
+ it.writeSerializable(type)
+ it.writeString(code)
+ it.writeSerializable(viewType)
+ writeBoolean(
+ it,
+ visible
+ )
+ writeBoolean(
+ it,
+ default
+ )
+ it.writeString(label)
+ it.writeParcelable(
+ value,
+ flags
+ )
+ writeBoolean(
+ it,
+ locked
+ )
+ }
+ }
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): EditableNomenclatureType {
+ return EditableNomenclatureType(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/NomenclatureFailure.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/NomenclatureFailure.kt
new file mode 100644
index 00000000..166ac922
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/NomenclatureFailure.kt
@@ -0,0 +1,19 @@
+package fr.geonature.occtax.features.nomenclature.error
+
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.NomenclatureType
+import fr.geonature.commons.error.Failure
+
+/**
+ * Failure about no [NomenclatureType] found locally.
+ *
+ * @author S. Grimault
+ */
+object NoNomenclatureTypeFoundLocallyFailure : Failure.FeatureFailure()
+
+/**
+ * Failure about no [Nomenclature] found from given mnemonic.
+ *
+ * @author S. Grimault
+ */
+data class NoNomenclatureValuesFoundFailure(val mnemonic: String) : Failure.FeatureFailure()
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/PropertyValueFailure.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/PropertyValueFailure.kt
new file mode 100644
index 00000000..3b6e005e
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/error/PropertyValueFailure.kt
@@ -0,0 +1,11 @@
+package fr.geonature.occtax.features.nomenclature.error
+
+import fr.geonature.commons.error.Failure
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * Failure about [PropertyValue].
+ *
+ * @author S. Grimault
+ */
+data class PropertyValueFailure(val code: String) : Failure.FeatureFailure()
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt
new file mode 100644
index 00000000..7c9e0d85
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/EditableNomenclatureTypeAdapter.kt
@@ -0,0 +1,444 @@
+package fr.geonature.occtax.features.nomenclature.presentation
+
+import android.text.Editable
+import android.text.InputType
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AutoCompleteTextView
+import android.widget.Button
+import androidx.core.content.res.ResourcesCompat
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.textfield.TextInputLayout
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.lifecycle.observeOnce
+import fr.geonature.commons.util.KeyboardUtils.hideSoftKeyboard
+import fr.geonature.occtax.R
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * Default RecyclerView Adapter about [EditableNomenclatureType].
+ *
+ * @author S. Grimault
+ */
+class EditableNomenclatureTypeAdapter(private val listener: OnEditableNomenclatureTypeAdapter) :
+ RecyclerView.Adapter() {
+
+ private val availableNomenclatureTypes = mutableListOf()
+ private val selectedNomenclatureTypes = mutableListOf()
+ private var showAllNomenclatureTypes = false
+ private var lockDefaultValues = false
+
+ init {
+ this.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ super.onChanged()
+
+ listener.showEmptyTextView(itemCount == 0)
+ }
+
+ override fun onItemRangeChanged(
+ positionStart: Int,
+ itemCount: Int
+ ) {
+ super.onItemRangeChanged(
+ positionStart,
+ itemCount
+ )
+
+ listener.showEmptyTextView(getItemCount() == 0)
+ }
+
+ override fun onItemRangeInserted(
+ positionStart: Int,
+ itemCount: Int
+ ) {
+ super.onItemRangeInserted(
+ positionStart,
+ itemCount
+ )
+
+ listener.showEmptyTextView(false)
+ }
+
+ override fun onItemRangeRemoved(
+ positionStart: Int,
+ itemCount: Int
+ ) {
+ super.onItemRangeRemoved(
+ positionStart,
+ itemCount
+ )
+
+ listener.showEmptyTextView(getItemCount() == 0)
+ }
+ })
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder {
+ return when (viewType) {
+ BaseEditableNomenclatureType.ViewType.NONE.ordinal -> MoreViewHolder(parent)
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE.ordinal -> TextSimpleViewHolder(parent)
+ BaseEditableNomenclatureType.ViewType.TEXT_MULTIPLE.ordinal -> TextMultipleViewHolder(parent)
+ else -> NomenclatureTypeViewHolder(parent)
+ }
+ }
+
+ override fun onBindViewHolder(holder: AbstractViewHolder, position: Int) {
+ selectedNomenclatureTypes[position].also {
+ holder.bind(it)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return selectedNomenclatureTypes.size
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return selectedNomenclatureTypes[position].viewType.ordinal
+ }
+
+ fun bind(
+ nomenclatureTypes: List,
+ vararg propertyValue: PropertyValue
+ ) {
+ availableNomenclatureTypes.clear()
+ availableNomenclatureTypes.addAll(
+ nomenclatureTypes.filter { it.visible }.map {
+ it.copy(value = propertyValue.firstOrNull { propertyValue -> propertyValue.code == it.code }
+ ?: it.value)
+ }
+ )
+
+ if (showAllNomenclatureTypes) showAllNomenclatureTypes(notify = true) else showDefaultNomenclatureTypes(notify = true)
+ }
+
+ fun showDefaultNomenclatureTypes(notify: Boolean = false) {
+ showAllNomenclatureTypes = false
+
+ if (availableNomenclatureTypes.isEmpty()) return
+
+ availableNomenclatureTypes.filter { it.default }.run {
+ if (isEmpty()) {
+ // nothing to show by default: show everything
+ showAllNomenclatureTypes(notify)
+ } else {
+ setSelectedNomenclatureTypes(
+ // show 'MORE' button only if we have some other editable nomenclatures to show
+ this + if (this.size < availableNomenclatureTypes.size) listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "MORE",
+ BaseEditableNomenclatureType.ViewType.NONE,
+ true
+ )
+ ) else emptyList()
+ )
+ }
+ }
+ }
+
+ fun showAllNomenclatureTypes(notify: Boolean = false) {
+ showAllNomenclatureTypes = true
+ setSelectedNomenclatureTypes(
+ availableNomenclatureTypes,
+ notify
+ )
+ }
+
+ fun lockDefaultValues(lock: Boolean = false) {
+ lockDefaultValues = lock
+ }
+
+ private fun setSelectedNomenclatureTypes(
+ nomenclatureTypes: List,
+ notify: Boolean = false
+ ) {
+ val oldKeys = selectedNomenclatureTypes.map { it.code }
+ val newKeys = nomenclatureTypes.map { it.code }
+
+ if (notify && oldKeys.isEmpty() && newKeys.isEmpty()) {
+ listener.showEmptyTextView(true)
+
+ return
+ }
+
+ val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = oldKeys.size
+
+ override fun getNewListSize(): Int = newKeys.size
+
+ override fun areItemsTheSame(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ) = oldKeys.elementAtOrNull(oldItemPosition) == newKeys.elementAtOrNull(newItemPosition)
+
+ override fun areContentsTheSame(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ) =
+ oldKeys.elementAtOrNull(oldItemPosition)
+ ?.let { code -> selectedNomenclatureTypes.firstOrNull { it.code == code } }
+ ?.value?.value == newKeys.elementAtOrNull(newItemPosition)
+ ?.let { code -> nomenclatureTypes.firstOrNull { it.code == code } }
+ ?.value?.value
+ })
+
+ selectedNomenclatureTypes.clear()
+ selectedNomenclatureTypes.addAll(nomenclatureTypes)
+
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+ abstract inner class AbstractViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal var nomenclatureType: EditableNomenclatureType? = null
+
+ fun bind(nomenclatureType: EditableNomenclatureType) {
+ this.nomenclatureType = nomenclatureType
+
+ onBind(nomenclatureType)
+ }
+
+ abstract fun onBind(nomenclatureType: EditableNomenclatureType)
+
+ /**
+ * Build the default label for given editable nomenclature type as fallback.
+ */
+ fun getNomenclatureTypeLabel(mnemonic: String): String {
+ return itemView.resources.getIdentifier(
+ "nomenclature_${mnemonic.lowercase()}",
+ "string",
+ itemView.context.packageName
+ ).takeIf { it > 0 }?.let { itemView.context.getString(it) } ?: mnemonic
+ }
+ }
+
+ inner class NomenclatureTypeViewHolder(parent: ViewGroup) : AbstractViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.view_action_nomenclature_type_select,
+ parent,
+ false
+ )
+ ) {
+ private var edit: TextInputLayout = itemView.findViewById(android.R.id.edit)
+ private var nomenclatureAdapter = NomenclatureValueAdapter(parent.context)
+ private var showDropdown = false
+
+ init {
+ (edit.editText as? AutoCompleteTextView)?.also {
+ it.setAdapter(nomenclatureAdapter)
+ it.setOnItemClickListener { _, _, position, _ ->
+ showDropdown = false
+ nomenclatureType?.run {
+ value = PropertyValue.fromNomenclature(
+ code,
+ nomenclatureAdapter.getNomenclatureValue(position)
+ )
+ listener.onUpdate(this)
+ }
+ }
+ }
+ }
+
+ override fun onBind(nomenclatureType: EditableNomenclatureType) {
+ if (!lockDefaultValues) {
+ nomenclatureType.locked = false
+ }
+
+ with(edit) {
+ startIconDrawable = if (lockDefaultValues) ResourcesCompat.getDrawable(
+ itemView.resources,
+ if (nomenclatureType.locked) R.drawable.ic_lock else R.drawable.ic_lock_open,
+ itemView.context.theme
+ ) else null
+ setStartIconOnClickListener {
+ if (!lockDefaultValues) return@setStartIconOnClickListener
+
+ nomenclatureType.locked = !nomenclatureType.locked
+ startIconDrawable = ResourcesCompat.getDrawable(
+ itemView.resources,
+ if (nomenclatureType.locked) R.drawable.ic_lock else R.drawable.ic_lock_open,
+ itemView.context.theme
+ )
+ listener.onUpdate(nomenclatureType)
+ }
+ hint = nomenclatureType.label ?: getNomenclatureTypeLabel(nomenclatureType.code)
+ setEndIconOnClickListener { setNomenclatureValues(nomenclatureType) }
+ (editText as? AutoCompleteTextView)?.apply {
+ setOnClickListener { setNomenclatureValues(nomenclatureType) }
+ text = nomenclatureType.value?.let {
+ Editable.Factory
+ .getInstance()
+ .newEditable(it.label ?: it.code)
+ }
+ }
+ }
+ }
+
+ private fun setNomenclatureValues(nomenclatureType: EditableNomenclatureType) {
+ if (showDropdown) {
+ showDropdown = false
+ (edit.editText as? AutoCompleteTextView)?.dismissDropDown()
+ return
+ }
+
+ listener.getNomenclatureValues(nomenclatureType.code)
+ .observeOnce(listener.getLifecycleOwner()) {
+ showDropdown = true
+ nomenclatureAdapter.setNomenclatureValues(it ?: listOf())
+ (edit.editText as? AutoCompleteTextView)?.showDropDown()
+ }
+ }
+ }
+
+ inner class MoreViewHolder(parent: ViewGroup) : AbstractViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.view_action_more,
+ parent,
+ false
+ )
+ ) {
+ private var button1: Button = itemView.findViewById(android.R.id.button1)
+
+ override fun onBind(nomenclatureType: EditableNomenclatureType) {
+ with(button1) {
+ text = getNomenclatureTypeLabel(nomenclatureType.code)
+ setOnClickListener {
+ showAllNomenclatureTypes()
+ listener.showMore()
+ }
+ }
+ }
+ }
+
+ open inner class TextSimpleViewHolder(parent: ViewGroup) : AbstractViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.view_action_edit_text,
+ parent,
+ false
+ )
+ ) {
+ internal var edit: TextInputLayout = itemView.findViewById(android.R.id.edit)
+ private val textWatcher = object : TextWatcher {
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int
+ ) {
+ }
+
+ override fun onTextChanged(
+ s: CharSequence?,
+ start: Int,
+ before: Int,
+ count: Int
+ ) {
+ }
+
+ override fun afterTextChanged(s: Editable?) {
+ nomenclatureType?.run {
+ value = PropertyValue.fromValue(
+ code,
+ s?.toString()?.ifEmpty { null }?.ifBlank { null }
+ )
+ listener.onUpdate(this)
+ }
+ }
+ }
+
+ init {
+ with(edit) {
+ editText?.addTextChangedListener(textWatcher)
+ setOnFocusChangeListener { v, hasFocus ->
+ if (!hasFocus) {
+ // workaround to force hide the soft keyboard
+ hideSoftKeyboard(v)
+ }
+ }
+ }
+ }
+
+ override fun onBind(nomenclatureType: EditableNomenclatureType) {
+ if (!lockDefaultValues) {
+ nomenclatureType.locked = false
+ }
+
+ with(edit) {
+ startIconDrawable = if (lockDefaultValues) ResourcesCompat.getDrawable(
+ itemView.resources,
+ if (nomenclatureType.locked) R.drawable.ic_lock else R.drawable.ic_lock_open,
+ itemView.context.theme
+ ) else null
+ setStartIconOnClickListener {
+ if (!lockDefaultValues) return@setStartIconOnClickListener
+
+ nomenclatureType.locked = !nomenclatureType.locked
+ startIconDrawable = ResourcesCompat.getDrawable(
+ itemView.resources,
+ if (nomenclatureType.locked) R.drawable.ic_lock else R.drawable.ic_lock_open,
+ itemView.context.theme
+ )
+ listener.onUpdate(nomenclatureType)
+ }
+ hint = getNomenclatureTypeLabel(nomenclatureType.code)
+ }
+
+ if (nomenclatureType.value?.value is String? && !(nomenclatureType.value?.value as String?).isNullOrEmpty()) {
+ edit.editText?.removeTextChangedListener(textWatcher)
+ edit.editText?.text = (nomenclatureType.value?.value as String?)?.let {
+ Editable.Factory.getInstance().newEditable(it)
+ }
+ edit.editText?.addTextChangedListener(textWatcher)
+ }
+ }
+ }
+
+ inner class TextMultipleViewHolder(parent: ViewGroup) : TextSimpleViewHolder(parent) {
+ init {
+ edit.isCounterEnabled = true
+ edit.editText?.apply {
+ isSingleLine = false
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
+ minLines = 2
+ maxLines = 4
+ }
+ }
+ }
+
+ /**
+ * Callback used by [EditableNomenclatureTypeAdapter].
+ */
+ interface OnEditableNomenclatureTypeAdapter {
+
+ fun getLifecycleOwner(): LifecycleOwner
+
+ /**
+ * Whether to show an empty text view when data changed.
+ */
+ fun showEmptyTextView(show: Boolean)
+
+ /**
+ * Called when the 'more' action button has been clicked.
+ */
+ fun showMore()
+
+ /**
+ * Requests showing all available nomenclature values from given nomenclature type.
+ */
+ fun getNomenclatureValues(nomenclatureTypeMnemonic: String): LiveData>
+
+ /**
+ * Called when an [EditableNomenclatureType] has been updated.
+ *
+ * @param editableNomenclatureType the [EditableNomenclatureType] updated
+ */
+ fun onUpdate(editableNomenclatureType: EditableNomenclatureType)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureValueAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureValueAdapter.kt
new file mode 100644
index 00000000..ae8ebaf4
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureValueAdapter.kt
@@ -0,0 +1,91 @@
+package fr.geonature.occtax.features.nomenclature.presentation
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.Filter
+import android.widget.Filterable
+import android.widget.TextView
+import fr.geonature.commons.data.entity.Nomenclature
+
+/**
+ * Default Adapter about [Nomenclature] values.
+ *
+ * @author S. Grimault
+ */
+class NomenclatureValueAdapter(context: Context) : BaseAdapter(), Filterable {
+ private val nomenclatureValues = mutableListOf()
+ private val filteredNomenclatureValues = mutableListOf()
+ private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
+ private val defaultFilter = DefaultFilter()
+
+ override fun getCount(): Int {
+ return filteredNomenclatureValues.size
+ }
+
+ override fun getItem(position: Int): String {
+ return filteredNomenclatureValues[position].defaultLabel
+ }
+
+ override fun getItemId(position: Int): Long {
+ return filteredNomenclatureValues[position].id
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ val view = convertView
+ ?: layoutInflater.inflate(
+ android.R.layout.simple_list_item_1,
+ parent,
+ false
+ )
+ view.findViewById(android.R.id.text1).text =
+ filteredNomenclatureValues[position].defaultLabel
+
+ return view
+ }
+
+ override fun getFilter(): Filter {
+ return defaultFilter
+ }
+
+ fun getNomenclatureValue(position: Int): Nomenclature {
+ return filteredNomenclatureValues[position]
+ }
+
+ fun setNomenclatureValues(nomenclatureValues: List) {
+ with(this.nomenclatureValues) {
+ clear()
+ addAll(nomenclatureValues)
+ }
+ with(this.filteredNomenclatureValues) {
+ clear()
+ addAll(nomenclatureValues)
+ }
+
+ notifyDataSetChanged()
+ }
+
+ inner class DefaultFilter : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ return filteredNomenclatureValues.filter { if (constraint == null) false else it.defaultLabel.contains(constraint) }
+ .let {
+ FilterResults().apply {
+ values = it
+ count = it.size
+ }
+ }
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ with(filteredNomenclatureValues) {
+ clear()
+ @Suppress("UNCHECKED_CAST")
+ addAll((results?.values as List?) ?: emptyList())
+ }
+
+ if (results?.count == 0) notifyDataSetInvalidated() else notifyDataSetChanged()
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModel.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModel.kt
new file mode 100644
index 00000000..84dbe356
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModel.kt
@@ -0,0 +1,86 @@
+package fr.geonature.occtax.features.nomenclature.presentation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.lifecycle.BaseViewModel
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.usecase.GetEditableNomenclaturesUseCase
+import fr.geonature.occtax.features.nomenclature.usecase.GetNomenclatureValuesByTypeAndTaxonomyUseCase
+import fr.geonature.occtax.settings.PropertySettings
+import javax.inject.Inject
+
+/**
+ * Nomenclature view model.
+ *
+ * @author S. Grimault
+ *
+ * @see GetEditableNomenclaturesUseCase
+ * @see GetNomenclatureValuesByTypeAndTaxonomyUseCase
+ */
+@HiltViewModel
+class NomenclatureViewModel @Inject constructor(
+ private val getEditableNomenclaturesUseCase: GetEditableNomenclaturesUseCase,
+ private val getNomenclatureValuesByTypeAndTaxonomyUseCase: GetNomenclatureValuesByTypeAndTaxonomyUseCase
+) :
+ BaseViewModel() {
+
+ private val _editableNomenclatures = MutableLiveData>()
+ val editableNomenclatures: LiveData> = _editableNomenclatures
+
+ /**
+ * Gets all editable nomenclatures from given type with default values.
+ *
+ * @param type the main editable nomenclature type
+ * @param defaultPropertySettings the default nomenclature settings
+ */
+ fun getEditableNomenclatures(
+ type: BaseEditableNomenclatureType.Type,
+ defaultPropertySettings: List = listOf(),
+ taxonomy: Taxonomy? = null
+ ) {
+ getEditableNomenclaturesUseCase(
+ GetEditableNomenclaturesUseCase.Params(
+ type,
+ defaultPropertySettings,
+ taxonomy
+ ),
+ viewModelScope
+ ) {
+ it.fold(::handleFailure) { editableNomenclatures ->
+ _editableNomenclatures.value = editableNomenclatures
+ }
+ }
+ }
+
+ /**
+ * Gets all nomenclature values matching given nomenclature type and an optional taxonomy rank.
+ *
+ * @param mnemonic the nomenclature type as main filter
+ * @param taxonomy the taxonomy rank
+ */
+ fun getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic: String,
+ taxonomy: Taxonomy? = null
+ ): LiveData> {
+ val nomenclatureValuesByTypeAndTaxonomy = MutableLiveData>()
+
+ getNomenclatureValuesByTypeAndTaxonomyUseCase(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic,
+ taxonomy
+ ),
+ viewModelScope
+ ) {
+ it.fold(::handleFailure) { nomenclatureValues ->
+ nomenclatureValuesByTypeAndTaxonomy.value = nomenclatureValues
+ }
+ }
+
+ return nomenclatureValuesByTypeAndTaxonomy
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/PropertyValueModel.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/PropertyValueModel.kt
new file mode 100644
index 00000000..dc8646a5
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/presentation/PropertyValueModel.kt
@@ -0,0 +1,85 @@
+package fr.geonature.occtax.features.nomenclature.presentation
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.lifecycle.BaseViewModel
+import fr.geonature.occtax.features.nomenclature.usecase.ClearDefaultPropertyValueUseCase
+import fr.geonature.occtax.features.nomenclature.usecase.SetDefaultPropertyValueUseCase
+import fr.geonature.occtax.input.PropertyValue
+import javax.inject.Inject
+
+/**
+ * [PropertyValue] view model.
+ *
+ * @author S. Grimault
+ *
+ * @see SetDefaultPropertyValueUseCase
+ */
+@HiltViewModel
+class PropertyValueModel @Inject constructor(
+ private val setDefaultPropertyValueUseCase: SetDefaultPropertyValueUseCase,
+ private val clearDefaultPropertyValueUseCase: ClearDefaultPropertyValueUseCase
+) :
+ BaseViewModel() {
+
+ /**
+ * Adds or updates given property value for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param propertyValue the property value to add or update
+ */
+ fun setPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ propertyValue: PropertyValue
+ ) {
+ setDefaultPropertyValueUseCase(
+ SetDefaultPropertyValueUseCase.Params(
+ taxonomy,
+ propertyValue
+ ),
+ viewModelScope
+ ) {
+ it.fold(::handleFailure) {}
+ }
+ }
+
+ /**
+ * Remove given property value by its code for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param code the property value code to remove
+ */
+ fun clearPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ code: String
+ ) {
+ clearDefaultPropertyValueUseCase(
+ ClearDefaultPropertyValueUseCase.Params.Params(
+ taxonomy,
+ code
+ ),
+ viewModelScope
+ ) {
+ it.fold(::handleFailure) {}
+ }
+ }
+
+ /**
+ * Clears all saved property values.
+ */
+ fun clearAllPropertyValues() {
+ clearDefaultPropertyValueUseCase(
+ ClearDefaultPropertyValueUseCase.Params.None,
+ viewModelScope
+ ) {
+ it.fold(::handleFailure) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryImpl.kt
new file mode 100644
index 00000000..8c553ef3
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryImpl.kt
@@ -0,0 +1,84 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureLocalDataSource
+import fr.geonature.occtax.features.nomenclature.data.IPropertyValueLocalDataSource
+import fr.geonature.occtax.features.nomenclature.error.PropertyValueFailure
+import fr.geonature.occtax.input.PropertyValue
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.toList
+
+/**
+ * Default implementation of [IDefaultPropertyValueRepository].
+ *
+ * @author S. Grimault
+ */
+class DefaultPropertyValueRepositoryImpl(
+ private val propertyValueLocalDataSource: IPropertyValueLocalDataSource,
+ private val nomenclatureLocalDataSource: INomenclatureLocalDataSource
+) :
+ IDefaultPropertyValueRepository {
+
+ override suspend fun getPropertyValues(taxonomy: Taxonomy?): Either> {
+ return Right(runCatching {
+ propertyValueLocalDataSource.getPropertyValues(
+ taxonomy ?: Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ )
+ )
+ }.getOrElse { emptyList() }
+ .asFlow()
+ .filter { propertyValue ->
+ runCatching {
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ propertyValue.code,
+ taxonomy
+ )
+ }.getOrElse { emptyList() }.takeIf { it.isNotEmpty() }
+ ?.any { it.id == propertyValue.value } ?: true
+ }
+ .toList())
+ }
+
+ override suspend fun setPropertyValue(
+ taxonomy: Taxonomy,
+ propertyValue: PropertyValue
+ ): Either {
+ return runCatching {
+ propertyValueLocalDataSource.setPropertyValue(
+ taxonomy,
+ propertyValue
+ )
+ }.fold(
+ onSuccess = { Right(Unit) },
+ onFailure = { Left(PropertyValueFailure(propertyValue.code)) }
+ )
+ }
+
+ override suspend fun clearPropertyValue(
+ taxonomy: Taxonomy,
+ code: String
+ ): Either {
+ return runCatching {
+ propertyValueLocalDataSource.clearPropertyValue(
+ taxonomy,
+ code
+ )
+ }.fold(
+ onSuccess = { Right(Unit) },
+ onFailure = { Left(PropertyValueFailure(code)) }
+ )
+ }
+
+ override suspend fun clearAllPropertyValues(): Either {
+ propertyValueLocalDataSource.clearAllPropertyValues()
+
+ return Right(Unit)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/IDefaultPropertyValueRepository.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/IDefaultPropertyValueRepository.kt
new file mode 100644
index 00000000..13e3b0f3
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/IDefaultPropertyValueRepository.kt
@@ -0,0 +1,54 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.occtax.input.PropertyValue
+
+/**
+ * Default [PropertyValue] repository.
+ *
+ * @author S. Grimault
+ */
+interface IDefaultPropertyValueRepository {
+
+ /**
+ * Gets all defined property values.
+ *
+ * @param taxonomy the taxonomy rank as filter
+ */
+ suspend fun getPropertyValues(taxonomy: Taxonomy? = null): Either>
+
+ /**
+ * Adds or updates given property value for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param propertyValue the property value to add or update
+ */
+ suspend fun setPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ propertyValue: PropertyValue
+ ): Either
+
+ /**
+ * Remove given property value by its code for the given given taxonomy rank.
+ *
+ * @param taxonomy the taxonomy rank
+ * @param code the property value code to remove
+ */
+ suspend fun clearPropertyValue(
+ taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ code: String
+ ): Either
+
+ /**
+ * Clears all saved property values.
+ */
+ suspend fun clearAllPropertyValues(): Either
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/INomenclatureRepository.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/INomenclatureRepository.kt
new file mode 100644
index 00000000..5febca0f
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/INomenclatureRepository.kt
@@ -0,0 +1,43 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.settings.PropertySettings
+
+/**
+ * Editable nomenclature types repository.
+ *
+ * @author S. Grimault
+ */
+interface INomenclatureRepository {
+
+ /**
+ * Gets all editable nomenclatures from given type with default values from nomenclature.
+ *
+ * @param type the main editable nomenclature type
+ * @param defaultPropertySettings the default nomenclature settings
+ *
+ * @return a list of [EditableNomenclatureType] or [Failure] if none was configured
+ */
+ suspend fun getEditableNomenclatures(
+ type: BaseEditableNomenclatureType.Type,
+ vararg defaultPropertySettings: PropertySettings
+ ): Either>
+
+ /**
+ * Gets all nomenclature values matching given nomenclature type and an optional taxonomy rank.
+ *
+ * @param mnemonic the nomenclature type as main filter
+ * @param taxonomy the taxonomy rank
+ *
+ * @return a list of [Nomenclature] matching given criteria or [Failure] if something goes wrong
+ */
+ suspend fun getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic: String,
+ taxonomy: Taxonomy? = null
+ ): Either>
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryImpl.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryImpl.kt
new file mode 100644
index 00000000..36da699f
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryImpl.kt
@@ -0,0 +1,107 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.NomenclatureWithType
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureLocalDataSource
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureSettingsLocalDataSource
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureTypeFoundLocallyFailure
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureValuesFoundFailure
+import fr.geonature.occtax.input.PropertyValue
+import fr.geonature.occtax.settings.PropertySettings
+import org.tinylog.Logger
+
+/**
+ * Default implementation of [INomenclatureRepository].
+ *
+ * @author S. Grimault
+ */
+class NomenclatureRepositoryImpl(
+ private val nomenclatureLocalDataSource: INomenclatureLocalDataSource,
+ private val nomenclatureSettingsLocalDataSource: INomenclatureSettingsLocalDataSource
+) : INomenclatureRepository {
+
+ override suspend fun getEditableNomenclatures(
+ type: BaseEditableNomenclatureType.Type,
+ vararg defaultPropertySettings: PropertySettings
+ ): Either> {
+ return runCatching {
+ val nomenclatureTypes =
+ nomenclatureLocalDataSource.getAllNomenclatureTypes().associateBy { it.mnemonic }
+
+ val defaultNomenclatureValues =
+ nomenclatureLocalDataSource.getAllDefaultNomenclatureValues().map { nomenclature ->
+ NomenclatureWithType(nomenclature).apply {
+ this.type =
+ nomenclatureTypes.entries.firstOrNull { it.value.id == typeId }?.value
+ }
+ }.filter { it.type != null }
+
+ nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(
+ type,
+ *defaultPropertySettings
+ ).mapNotNull {
+ if (it.viewType == BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE) nomenclatureTypes[it.code]?.let { nomenclatureType ->
+ EditableNomenclatureType(
+ it.type,
+ it.code,
+ it.viewType,
+ it.visible,
+ it.default,
+ nomenclatureType.defaultLabel.takeIf { label -> label.isNotEmpty() }
+ ?: run {
+ Logger.warn { "no label found for nomenclature type '${nomenclatureType.mnemonic}', use default…" }
+ null
+ }
+ )
+ } else EditableNomenclatureType(
+ it.type,
+ it.code,
+ it.viewType,
+ it.visible,
+ it.default
+ )
+ }.map { editableNomenclature ->
+ editableNomenclature.copy(value = defaultNomenclatureValues.firstOrNull { it.type?.mnemonic == editableNomenclature.code }
+ ?.let {
+ PropertyValue.fromNomenclature(
+ editableNomenclature.code,
+ it
+ )
+ })
+ }
+ }.fold(
+ onSuccess = {
+ if (it.isEmpty()) Left(NoNomenclatureTypeFoundLocallyFailure) else Right(it)
+ },
+ onFailure = {
+ Left(Failure.DbFailure(it))
+ }
+ )
+ }
+
+ override suspend fun getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic: String,
+ taxonomy: Taxonomy?
+ ): Either> {
+ return runCatching {
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic,
+ taxonomy
+ )
+ }.fold(
+ onSuccess = {
+ if (it.isEmpty()) Left(NoNomenclatureValuesFoundFailure(mnemonic)) else Right(it)
+ },
+ onFailure = {
+ Left(Failure.DbFailure(it))
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCase.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCase.kt
new file mode 100644
index 00000000..00fc2260
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCase.kt
@@ -0,0 +1,41 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.interactor.BaseUseCase
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import javax.inject.Inject
+
+/**
+ * Remove given property value by its code for the given given taxonomy rank.
+ * If no property value code is given, clears all saved property values.
+ *
+ * @author S. Grimault
+ */
+class ClearDefaultPropertyValueUseCase @Inject constructor(
+ private val defaultPropertyValueRepository: IDefaultPropertyValueRepository
+) :
+ BaseUseCase() {
+ override suspend fun run(params: Params): Either {
+ return when (params) {
+ Params.None -> defaultPropertyValueRepository.clearAllPropertyValues()
+ is Params.Params -> defaultPropertyValueRepository.clearPropertyValue(
+ params.taxonomy,
+ params.code
+ )
+ }
+ }
+
+ sealed class Params {
+ data class Params(
+ val taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ val code: String
+ ) : ClearDefaultPropertyValueUseCase.Params()
+
+ object None : ClearDefaultPropertyValueUseCase.Params()
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCase.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCase.kt
new file mode 100644
index 00000000..3d38832c
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCase.kt
@@ -0,0 +1,62 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.commons.fp.getOrElse
+import fr.geonature.commons.interactor.BaseUseCase
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureTypeFoundLocallyFailure
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import fr.geonature.occtax.features.nomenclature.repository.INomenclatureRepository
+import fr.geonature.occtax.settings.PropertySettings
+import javax.inject.Inject
+
+/**
+ * Gets all editable nomenclatures from given type with default values.
+ *
+ * @author S. Grimault
+ */
+class GetEditableNomenclaturesUseCase @Inject constructor(
+ private val nomenclatureRepository: INomenclatureRepository,
+ private val defaultPropertyValueRepository: IDefaultPropertyValueRepository
+) :
+ BaseUseCase, GetEditableNomenclaturesUseCase.Params>() {
+ override suspend fun run(params: Params): Either> {
+ val editableNomenclaturesResult = nomenclatureRepository.getEditableNomenclatures(
+ params.type,
+ *params.settings.toTypedArray()
+ )
+
+ if (editableNomenclaturesResult.isLeft) {
+ return editableNomenclaturesResult
+ }
+
+ val editableNomenclatures = editableNomenclaturesResult.getOrElse { emptyList() }
+
+ if (editableNomenclatures.isEmpty()) {
+ return Left(NoNomenclatureTypeFoundLocallyFailure)
+ }
+
+ val defaultPropertyValues =
+ defaultPropertyValueRepository.getPropertyValues(params.taxonomy)
+ .getOrElse { emptyList() }
+
+ return Right(editableNomenclatures.map { editableNomenclature ->
+ editableNomenclature.copy(
+ value = defaultPropertyValues.firstOrNull { it.code == editableNomenclature.code }
+ ?: editableNomenclature.value,
+ locked = defaultPropertyValues.any { it.code == editableNomenclature.code }
+ )
+ })
+ }
+
+ data class Params(
+ val type: BaseEditableNomenclatureType.Type,
+ val settings: List = listOf(),
+ val taxonomy: Taxonomy? = null
+ )
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCase.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCase.kt
new file mode 100644
index 00000000..ca55fe03
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCase.kt
@@ -0,0 +1,29 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.interactor.BaseUseCase
+import fr.geonature.occtax.features.nomenclature.repository.INomenclatureRepository
+import javax.inject.Inject
+
+/**
+ * Gets all nomenclature values matching given nomenclature type and an optional taxonomy rank.
+ *
+ * @author S. Grimault
+ */
+class GetNomenclatureValuesByTypeAndTaxonomyUseCase @Inject constructor(private val nomenclatureRepository: INomenclatureRepository) :
+ BaseUseCase, GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params>() {
+ override suspend fun run(params: Params): Either> {
+ return nomenclatureRepository.getNomenclatureValuesByTypeAndTaxonomy(
+ params.mnemonic,
+ params.taxonomy
+ )
+ }
+
+ data class Params(
+ val mnemonic: String,
+ val taxonomy: Taxonomy? = null
+ )
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCase.kt b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCase.kt
new file mode 100644
index 00000000..bb184355
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCase.kt
@@ -0,0 +1,34 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either
+import fr.geonature.commons.interactor.BaseUseCase
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import fr.geonature.occtax.input.PropertyValue
+import javax.inject.Inject
+
+/**
+ * Adds or updates given property value for the given given taxonomy rank.
+ *
+ * @author S. Grimault
+ */
+class SetDefaultPropertyValueUseCase @Inject constructor(
+ private val defaultPropertyValueRepository: IDefaultPropertyValueRepository
+) :
+ BaseUseCase() {
+ override suspend fun run(params: Params): Either {
+ return defaultPropertyValueRepository.setPropertyValue(
+ params.taxonomy,
+ params.propertyValue
+ )
+ }
+
+ data class Params(
+ val taxonomy: Taxonomy = Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ val propertyValue: PropertyValue
+ )
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/input/CountingMetadata.kt b/occtax/src/main/java/fr/geonature/occtax/input/CountingMetadata.kt
index 60b00980..7ac0e936 100644
--- a/occtax/src/main/java/fr/geonature/occtax/input/CountingMetadata.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/input/CountingMetadata.kt
@@ -16,7 +16,7 @@ class CountingMetadata() : Parcelable {
internal set
val properties: SortedMap =
- TreeMap { o1, o2 ->
+ TreeMap { o1, o2 ->
val i1 = defaultMnemonic.indexOfFirst { it.first == o1 }
val i2 = defaultMnemonic.indexOfFirst { it.first == o2 }
@@ -100,6 +100,7 @@ class CountingMetadata() : Parcelable {
* * first value: mnemonic code from nomenclature type
* * second value: the corresponding view type
*/
+ @Deprecated("see: INomenclatureSettingsLocalDataSource")
val defaultMnemonic = arrayOf(
Pair(
"STADE_VIE",
diff --git a/occtax/src/main/java/fr/geonature/occtax/input/Input.kt b/occtax/src/main/java/fr/geonature/occtax/input/Input.kt
index 89e27de0..213e832f 100644
--- a/occtax/src/main/java/fr/geonature/occtax/input/Input.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/input/Input.kt
@@ -85,6 +85,7 @@ class Input : AbstractInput {
companion object {
+ @Deprecated("see: INomenclatureSettingsLocalDataSource")
val defaultPropertiesMnemonic = arrayOf(
Pair(
"TYP_GRP",
diff --git a/occtax/src/main/java/fr/geonature/occtax/input/InputTaxon.kt b/occtax/src/main/java/fr/geonature/occtax/input/InputTaxon.kt
index 88ed345e..f7b339d6 100644
--- a/occtax/src/main/java/fr/geonature/occtax/input/InputTaxon.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/input/InputTaxon.kt
@@ -15,16 +15,7 @@ import java.util.TreeMap
class InputTaxon : AbstractInputTaxon {
val properties: SortedMap =
- TreeMap { o1, o2 ->
- val i1 = defaultPropertiesMnemonic.indexOfFirst { it.first == o1 }
- val i2 = defaultPropertiesMnemonic.indexOfFirst { it.first == o2 }
-
- when {
- i1 == -1 -> 1
- i2 == -1 -> -1
- else -> i1 - i2
- }
- }
+ TreeMap { o1, o2 -> o1.compareTo(o2) }
private val counting: SortedMap = TreeMap()
constructor(taxon: AbstractTaxon) : super(taxon)
@@ -96,61 +87,6 @@ class InputTaxon : AbstractInputTaxon {
companion object {
- /**
- * default properties as triple:
- *
- * * first value: mnemonic code from nomenclature type
- * * second value: the corresponding view type
- * * third value: if this property is visible by default
- */
- val defaultPropertiesMnemonic = arrayOf(
- Triple(
- "METH_OBS",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- true
- ),
- Triple(
- "ETA_BIO",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- true
- ),
- Triple(
- "METH_DETERMIN",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- false
- ),
- Triple(
- "DETERMINER",
- NomenclatureTypeViewType.TEXT_SIMPLE,
- false
- ),
- Triple(
- "STATUT_BIO",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- false
- ),
- Triple(
- "OCC_COMPORTEMENT",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- false
- ),
- Triple(
- "NATURALITE",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- false
- ),
- Triple(
- "PREUVE_EXIST",
- NomenclatureTypeViewType.NOMENCLATURE_TYPE,
- false
- ),
- Triple(
- "COMMENT",
- NomenclatureTypeViewType.TEXT_MULTIPLE,
- false
- )
- )
-
@JvmField
val CREATOR: Parcelable.Creator = object : Parcelable.Creator {
diff --git a/occtax/src/main/java/fr/geonature/occtax/input/NomenclatureTypeViewType.kt b/occtax/src/main/java/fr/geonature/occtax/input/NomenclatureTypeViewType.kt
index 5057021d..127a0272 100644
--- a/occtax/src/main/java/fr/geonature/occtax/input/NomenclatureTypeViewType.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/input/NomenclatureTypeViewType.kt
@@ -5,6 +5,7 @@ package fr.geonature.occtax.input
*
* @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
*/
+@Deprecated(message = "see EditableNomenclatureType")
enum class NomenclatureTypeViewType {
NOMENCLATURE_TYPE,
MORE,
diff --git a/occtax/src/main/java/fr/geonature/occtax/input/PropertyValue.kt b/occtax/src/main/java/fr/geonature/occtax/input/PropertyValue.kt
index ea782abc..3fde6215 100644
--- a/occtax/src/main/java/fr/geonature/occtax/input/PropertyValue.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/input/PropertyValue.kt
@@ -4,6 +4,7 @@ import android.os.Parcel
import android.os.Parcelable
import fr.geonature.commons.data.entity.Nomenclature
import fr.geonature.commons.input.AbstractInputTaxon
+import org.tinylog.Logger
import java.io.Serializable
/**
@@ -17,7 +18,7 @@ data class PropertyValue(
val value: Serializable?
) : Parcelable {
- internal constructor(source: Parcel) : this(
+ private constructor(source: Parcel) : this(
source.readString()!!,
source.readString(),
source.readSerializable()
@@ -51,6 +52,10 @@ data class PropertyValue(
code: String,
nomenclature: Nomenclature?
): PropertyValue {
+ if (nomenclature?.defaultLabel.isNullOrEmpty()) {
+ Logger.warn { "no label found for nomenclature '$code:${nomenclature?.code}'" }
+ }
+
return PropertyValue(
code,
nomenclature?.defaultLabel,
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt b/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
index bc934544..8bae16f2 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/NomenclatureSettings.kt
@@ -2,17 +2,21 @@ package fr.geonature.occtax.settings
import android.os.Parcel
import android.os.Parcelable
+import androidx.core.os.ParcelCompat
+import androidx.core.os.ParcelCompat.readBoolean
/**
* Nomenclature settings.
*
- * @author [S. Grimault](mailto:sebastien.grimault@gmail.com)
+ * @author S. Grimault
*/
data class NomenclatureSettings(
+ val saveDefaultValues: Boolean = false,
val information: List,
val counting: List
) : Parcelable {
private constructor(source: Parcel) : this(
+ readBoolean(source),
mutableListOf(),
mutableListOf()
) {
@@ -35,6 +39,10 @@ data class NomenclatureSettings(
flags: Int
) {
dest?.also {
+ ParcelCompat.writeBoolean(
+ it,
+ saveDefaultValues
+ )
it.writeTypedList(information)
it.writeTypedList(counting)
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt b/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
index 35eb8274..c8bc2838 100644
--- a/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/settings/io/OnAppSettingsJsonReaderListenerImpl.kt
@@ -125,6 +125,7 @@ class OnAppSettingsJsonReaderListenerImpl :
return null
}
+ var saveDefaultValues = false
val information = mutableListOf()
val counting = mutableListOf()
@@ -132,6 +133,7 @@ class OnAppSettingsJsonReaderListenerImpl :
while (reader.hasNext()) {
when (reader.nextName()) {
+ "save_default_values" -> saveDefaultValues = reader.nextBooleanOrElse { false }
"information" -> information.addAll(readPropertySettingsAsList(reader))
"counting" -> counting.addAll(readPropertySettingsAsList(reader))
}
@@ -140,8 +142,9 @@ class OnAppSettingsJsonReaderListenerImpl :
reader.endObject()
return NomenclatureSettings(
- information,
- counting
+ saveDefaultValues = saveDefaultValues,
+ information = information,
+ counting = counting
)
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt
index 12fcf2a8..01893ca6 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/dataset/DatasetRecyclerViewAdapter.kt
@@ -5,7 +5,6 @@ import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.CheckBox
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.l4digital.fastscroll.FastScroller
@@ -36,15 +35,9 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap
val text1: TextView = v.findViewById(android.R.id.text1)
text1.isSelected = true
- val checkbox: CheckBox = v.findViewById(android.R.id.checkbox)
- checkbox.isChecked = !checkbox.isChecked
-
val dataset = v.tag as Dataset
-
- if (checkbox.isChecked) {
- selectedDataset = dataset
- listener.onSelectedDataset(dataset)
- }
+ selectedDataset = dataset
+ listener.onSelectedDataset(dataset)
if (previousSelectedItemPosition >= 0) {
notifyItemChanged(previousSelectedItemPosition)
@@ -133,7 +126,7 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap
inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(
- R.layout.list_selectable_item_3,
+ R.layout.list_item_dataset,
parent,
false
)
@@ -142,7 +135,6 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap
private val title: TextView = itemView.findViewById(android.R.id.title)
private val text1: TextView = itemView.findViewById(android.R.id.text1)
private val text2: TextView = itemView.findViewById(android.R.id.text2)
- private val checkbox: CheckBox = itemView.findViewById(android.R.id.checkbox)
fun bind(position: Int) {
val cursor = cursor ?: return
@@ -154,6 +146,7 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap
if (dataset != null) {
title.text = dataset.name
title.isSelected = selectedDataset?.id == dataset.id
+ title.isSelected = true
text1.text = dataset.description
text1.isSelected = selectedDataset?.id == dataset.id
text2.text = itemView.context.getString(
@@ -163,9 +156,9 @@ class DatasetRecyclerViewAdapter(private val listener: OnDatasetRecyclerViewAdap
dataset.createdAt
)
)
- checkbox.isChecked = selectedDataset?.id == dataset.id
with(itemView) {
+ isPressed = selectedDataset?.id == dataset.id
tag = dataset
setOnClickListener(onClickListener)
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
index f9ffe538..d7e038ee 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/home/HomeActivity.kt
@@ -421,6 +421,7 @@ class HomeActivity : AppCompatActivity() {
vm.isSyncRunning.observe(
this@HomeActivity
) {
+ appSyncView?.enableActionButton(!it)
invalidateOptionsMenu()
}
vm
@@ -429,7 +430,7 @@ class HomeActivity : AppCompatActivity() {
this@HomeActivity
) {
if (it == null) {
- appSyncView?.setDataSyncStatus(it)
+ appSyncView?.setDataSyncStatus(null)
}
it?.run {
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/AbstractInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/AbstractInputFragment.kt
new file mode 100644
index 00000000..dafc4c4b
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/AbstractInputFragment.kt
@@ -0,0 +1,59 @@
+package fr.geonature.occtax.ui.input
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import fr.geonature.commons.input.AbstractInput
+import fr.geonature.commons.lifecycle.observeUntil
+import fr.geonature.occtax.input.Input
+import fr.geonature.occtax.input.InputViewModel
+import fr.geonature.viewpager.model.IPageWithValidationFragment
+
+/**
+ * `Fragment` using [AbstractInput] as page.
+ *
+ * @author S. Grimault
+ */
+abstract class AbstractInputFragment : Fragment(), IPageWithValidationFragment, IInputFragment {
+
+ lateinit var listener: OnInputPageFragmentListener
+
+ private val inputViewModel: InputViewModel by activityViewModels()
+ var input: Input? = null
+ private set
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ if (context is OnInputPageFragmentListener) {
+ listener = context
+ } else {
+ throw RuntimeException("$context must implement ${OnInputPageFragmentListener::class.simpleName}")
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // give a chance to this page to refresh correctly both its title and subtitle
+ Handler(Looper.getMainLooper()).post {
+ (activity as AppCompatActivity?)?.apply {
+ setTitle(getResourceTitle())
+ supportActionBar?.subtitle = getSubtitle()
+ }
+ }
+
+ inputViewModel.input.observeUntil(
+ viewLifecycleOwner,
+ { it != null }) {
+ if (it == null) return@observeUntil
+
+ input = it
+ listener.validateCurrentPage()
+ refreshView()
+ }
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/IInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/IInputFragment.kt
index c7469dd0..2fded1f3 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/IInputFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/IInputFragment.kt
@@ -10,9 +10,7 @@ import fr.geonature.commons.input.AbstractInput
interface IInputFragment {
/**
- * Sets the current [AbstractInput] to update.
- *
- * @param input the current [AbstractInput] to update
+ * Updates the current view.
*/
- fun setInput(input: AbstractInput)
+ fun refreshView()
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt
index c381c026..82add334 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/InputPagerFragmentActivity.kt
@@ -3,13 +3,17 @@ package fr.geonature.occtax.ui.input
import android.Manifest
import android.content.Context
import android.content.Intent
+import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import androidx.activity.viewModels
+import androidx.core.graphics.ColorUtils
+import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
-import androidx.viewpager.widget.ViewPager
import dagger.hilt.android.AndroidEntryPoint
import fr.geonature.commons.input.AbstractInput
+import fr.geonature.commons.util.KeyboardUtils.hideKeyboard
+import fr.geonature.commons.util.ThemeUtils
import fr.geonature.maps.settings.MapSettings
import fr.geonature.maps.ui.MapFragment
import fr.geonature.maps.util.CheckPermissionLifecycleObserver
@@ -27,23 +31,21 @@ import fr.geonature.occtax.ui.input.map.InputMapFragment
import fr.geonature.occtax.ui.input.observers.ObserversAndDateInputFragment
import fr.geonature.occtax.ui.input.summary.InputTaxaSummaryFragment
import fr.geonature.occtax.ui.input.taxa.TaxaFragment
-import fr.geonature.viewpager.ui.AbstractNavigationHistoryPagerFragmentActivity
+import fr.geonature.viewpager.model.IPageWithValidationFragment
import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.tinylog.kotlin.Logger
import kotlin.coroutines.resume
/**
- * [ViewPager] implementation as [AbstractPagerFragmentActivity] with navigation history support.
+ * `ViewPager2` implementation through [AbstractPagerFragmentActivity].
*
* @author S. Grimault
*/
@AndroidEntryPoint
-class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivity(),
+class InputPagerFragmentActivity : AbstractPagerFragmentActivity(),
+ OnInputPageFragmentListener,
MapFragment.OnMapFragmentPermissionsListener {
private val inputViewModel: InputViewModel by viewModels()
@@ -60,6 +62,10 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ // FIXME: this is a workaround to keep MapView alive from InputMapFragment…
+ // see: https://github.com/osmdroid/osmdroid/issues/1581
+ viewPager.offscreenPageLimit = 6
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
manageExternalStoragePermissionLifecycleObserver =
ManageExternalStoragePermissionLifecycleObserver(this)
@@ -84,74 +90,110 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit
}
Logger.info { "loading input: ${input.id}" }
-
- CoroutineScope(Dispatchers.Main).launch {
- pagerManager.load(input.id)
- }
+ inputViewModel.editInput(input)
+
+ pageFragmentViewModel.set(
+ R.string.pager_fragment_observers_and_date_input_title to ObserversAndDateInputFragment.newInstance(
+ dateSettings = appSettings.inputSettings.dateSettings,
+ saveDefaultValues = appSettings.nomenclatureSettings?.saveDefaultValues ?: false
+ ),
+ R.string.pager_fragment_map_title to InputMapFragment.newInstance(
+ MapSettings.Builder.newInstance()
+ .from(appSettings.mapSettings!!)
+ .showCompass(showCompass(this))
+ .showScale(showScale(this))
+ .showZoom(showZoom(this))
+ .build()
+ ),
+ R.string.pager_fragment_summary_title to InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings)
+ )
}
override fun onPause() {
super.onPause()
- if (input.status == AbstractInput.Status.DRAFT) {
- inputViewModel.saveInput(input)
+ inputViewModel.input.value?.takeIf { it.status == AbstractInput.Status.DRAFT }?.also {
+ inputViewModel.saveInput(it)
}
}
- override val pagerFragments: Map
- get() = LinkedHashMap().apply {
- put(
- R.string.pager_fragment_observers_and_date_input_title,
- ObserversAndDateInputFragment.newInstance(appSettings.inputSettings.dateSettings)
- )
- put(
- R.string.pager_fragment_map_title,
- InputMapFragment.newInstance(getMapSettings())
- )
- put(
- R.string.pager_fragment_taxa_title,
- TaxaFragment.newInstance(appSettings.areaObservationDuration)
- )
- put(
- R.string.pager_fragment_information_title,
- InformationFragment.newInstance(
- *appSettings.nomenclatureSettings?.information?.toTypedArray() ?: emptyArray()
- )
- )
- put(
- R.string.pager_fragment_counting_title,
- CountingFragment.newInstance(
- *appSettings.nomenclatureSettings?.counting?.toTypedArray() ?: emptyArray()
- )
- )
- put(
- R.string.pager_fragment_summary_title,
- InputTaxaSummaryFragment.newInstance()
- )
- }
+ override fun getDefaultTitle(): CharSequence {
+ return getString(R.string.activity_input_title)
+ }
+
+ override fun onNextAction(): Boolean {
+ return false
+ }
override fun performFinishAction() {
- inputViewModel.exportInput(
- input,
- appSettings
- ) {
- finish()
+ inputViewModel.input.value?.also {
+ inputViewModel.exportInput(
+ it,
+ appSettings
+ ) {
+ finish()
+ }
}
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
- val pageFragment = getCurrentPageFragment()
+ getCurrentPageFragment()?.also { page ->
+ if (page is IPageWithValidationFragment) {
+ // override the default next button color for the last page
+ nextButton.backgroundTintList = ColorStateList(
+ arrayOf(
+ intArrayOf(-android.R.attr.state_enabled),
+ IntArray(0)
+ ),
+ intArrayOf(
+ ColorUtils.setAlphaComponent(
+ ThemeUtils.getColor(
+ this,
+ R.attr.colorOnSurface
+ ),
+ 32
+ ),
+ if (position < ((viewPager.adapter?.itemCount
+ ?: 0) - 1)
+ ) ThemeUtils.getAccentColor(this)
+ else ThemeUtils.getPrimaryColor(this)
+ )
+ )
- if (pageFragment is IInputFragment && ::input.isInitialized) {
- pageFragment.setInput(input)
- pageFragment.refreshView()
- validateCurrentPage()
- inputViewModel.saveInput(input)
+ hideKeyboard(page as Fragment)
+ }
}
}
+ override fun startEditTaxon() {
+ pageFragmentViewModel.add(
+ R.string.pager_fragment_taxa_title to TaxaFragment.newInstance(appSettings.areaObservationDuration),
+ R.string.pager_fragment_information_title to InformationFragment.newInstance(
+ saveDefaultValues = appSettings.nomenclatureSettings?.saveDefaultValues ?: false,
+ *appSettings.nomenclatureSettings?.information?.toTypedArray()
+ ?: emptyArray()
+ ),
+ R.string.pager_fragment_counting_title to CountingFragment.newInstance(
+ *appSettings.nomenclatureSettings?.counting?.toTypedArray()
+ ?: emptyArray()
+ ),
+ R.string.pager_fragment_taxa_added_title to InputTaxaSummaryFragment.newInstance(appSettings.inputSettings.dateSettings)
+ )
+ goToNextPage()
+ }
+
+ override fun finishEditTaxon() {
+ input.clearCurrentSelectedInputTaxon()
+ removePage(
+ R.string.pager_fragment_taxa_title,
+ R.string.pager_fragment_information_title,
+ R.string.pager_fragment_counting_title,
+ R.string.pager_fragment_taxa_added_title
+ )
+ }
+
override suspend fun onStoragePermissionsGranted() =
suspendCancellableCoroutine { continuation ->
lifecycleScope.launch {
@@ -175,15 +217,6 @@ class InputPagerFragmentActivity : AbstractNavigationHistoryPagerFragmentActivit
}
}
- private fun getMapSettings(): MapSettings {
- return MapSettings.Builder.newInstance()
- .from(appSettings.mapSettings!!)
- .showCompass(showCompass(this))
- .showScale(showScale(this))
- .showZoom(showZoom(this))
- .build()
- }
-
companion object {
private const val EXTRA_APP_SETTINGS = "extra_app_settings"
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt
new file mode 100644
index 00000000..b1a7c2b0
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/OnInputPageFragmentListener.kt
@@ -0,0 +1,21 @@
+package fr.geonature.occtax.ui.input
+
+import fr.geonature.viewpager.ui.OnPageFragmentListener
+
+/**
+ * Callback used within pages to control [InputPagerFragmentActivity] view pager.
+ *
+ * @author S. Grimault
+ */
+interface OnInputPageFragmentListener : OnPageFragmentListener {
+
+ /**
+ * Start taxon editing workflow.
+ */
+ fun startEditTaxon()
+
+ /**
+ * Finish taxon editing workflow.
+ */
+ fun finishEditTaxon()
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt
index 191954e3..66836622 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/CountingFragment.kt
@@ -21,29 +21,23 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import fr.geonature.commons.data.entity.Taxonomy
-import fr.geonature.commons.input.AbstractInput
import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter
import fr.geonature.occtax.R
import fr.geonature.occtax.input.CountingMetadata
import fr.geonature.occtax.input.Input
import fr.geonature.occtax.input.InputTaxon
import fr.geonature.occtax.settings.PropertySettings
-import fr.geonature.occtax.ui.input.IInputFragment
-import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
+import fr.geonature.occtax.ui.input.AbstractInputFragment
/**
* [Fragment] to let the user to add additional counting information for the given [Input].
*
* @author S. Grimault
*/
-class CountingFragment : Fragment(),
- IValidateFragment,
- IInputFragment {
+class CountingFragment : AbstractInputFragment() {
private lateinit var editCountingResultLauncher: ActivityResultLauncher
- private var input: Input? = null
private var adapter: CountingRecyclerViewAdapter? = null
private var recyclerView: RecyclerView? = null
private var emptyTextView: TextView? = null
@@ -125,7 +119,7 @@ class CountingFragment : Fragment(),
) { dialog, _ ->
adapter?.remove(item)
(input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.deleteCountingMetadata(item.index)
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
dialog.dismiss()
}
@@ -205,10 +199,6 @@ class CountingFragment : Fragment(),
}
}
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
- }
-
private fun launchEditCountingMetadataActivity(countingMetadata: CountingMetadata? = null) {
val context = context ?: return
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/NomenclatureTypesRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/NomenclatureTypesRecyclerViewAdapter.kt
index adcb7952..caf3ea2c 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/NomenclatureTypesRecyclerViewAdapter.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/counting/NomenclatureTypesRecyclerViewAdapter.kt
@@ -28,6 +28,7 @@ import kotlin.math.ceil
*
* @author S. Grimault
*/
+@Deprecated("use EditableNomenclatureTypeAdapter")
class NomenclatureTypesRecyclerViewAdapter(private val listener: OnNomenclatureTypesRecyclerViewAdapterListener) :
RecyclerView.Adapter() {
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt
index 19389919..559e080d 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/InformationFragment.kt
@@ -1,34 +1,33 @@
package fr.geonature.occtax.ui.input.informations
-import android.database.Cursor
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.view.setPadding
+import android.view.animation.AnimationUtils
+import android.widget.ProgressBar
+import android.widget.TextView
import androidx.fragment.app.Fragment
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.CursorLoader
-import androidx.loader.content.Loader
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import fr.geonature.commons.data.ContentProviderAuthority
-import fr.geonature.commons.data.entity.DefaultNomenclatureWithType
import fr.geonature.commons.data.entity.Nomenclature
-import fr.geonature.commons.data.entity.NomenclatureType
import fr.geonature.commons.data.entity.Taxonomy
-import fr.geonature.commons.data.helper.ProviderHelper.buildUri
-import fr.geonature.commons.input.AbstractInput
+import fr.geonature.commons.lifecycle.observe
import fr.geonature.occtax.R
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.presentation.EditableNomenclatureTypeAdapter
+import fr.geonature.occtax.features.nomenclature.presentation.NomenclatureViewModel
+import fr.geonature.occtax.features.nomenclature.presentation.PropertyValueModel
import fr.geonature.occtax.input.Input
import fr.geonature.occtax.input.InputTaxon
-import fr.geonature.occtax.input.PropertyValue
import fr.geonature.occtax.settings.PropertySettings
-import fr.geonature.occtax.ui.input.IInputFragment
-import fr.geonature.occtax.ui.input.dialog.ChooseNomenclatureDialogFragment
-import fr.geonature.viewpager.ui.IValidateFragment
-import org.tinylog.kotlin.Logger
+import fr.geonature.occtax.ui.input.AbstractInputFragment
import javax.inject.Inject
/**
@@ -37,109 +36,30 @@ import javax.inject.Inject
* @author S. Grimault
*/
@AndroidEntryPoint
-class InformationFragment : Fragment(),
- IValidateFragment,
- IInputFragment,
- ChooseNomenclatureDialogFragment.OnChooseNomenclatureDialogFragmentListener {
+class InformationFragment : AbstractInputFragment() {
@ContentProviderAuthority
@Inject
lateinit var authority: String
- private var input: Input? = null
- private var adapter: NomenclatureTypesRecyclerViewAdapter? = null
- private lateinit var savedState: Bundle
-
- private val loaderCallbacks = object : LoaderManager.LoaderCallbacks {
- override fun onCreateLoader(
- id: Int,
- args: Bundle?
- ): Loader {
- return when (id) {
- LOADER_NOMENCLATURE_TYPES -> CursorLoader(
- requireContext(),
- buildUri(
- authority,
- NomenclatureType.TABLE_NAME
- ),
- null,
- null,
- null,
- null
- )
- LOADER_DEFAULT_NOMENCLATURE_VALUES -> CursorLoader(
- requireContext(),
- buildUri(
- authority,
- NomenclatureType.TABLE_NAME,
- "occtax",
- "default"
- ),
- null,
- null,
- null,
- null
- )
- else -> throw IllegalArgumentException()
- }
- }
-
- override fun onLoadFinished(
- loader: Loader,
- data: Cursor?
- ) {
- if (data == null) {
- Logger.warn { "failed to load data from '${(loader as CursorLoader).uri}'" }
-
- return
- }
+ private val nomenclatureViewModel: NomenclatureViewModel by viewModels()
+ private val propertyValueModel: PropertyValueModel by viewModels()
- when (loader.id) {
- LOADER_NOMENCLATURE_TYPES -> {
- val defaultProperties = arguments?.getParcelableArray(ARG_PROPERTIES)
- ?.map { it as PropertySettings }
- ?.toTypedArray() ?: emptyArray()
-
- adapter?.bind(
- data,
- *defaultProperties
- )
- loadDefaultNomenclatureValues()
- }
- LOADER_DEFAULT_NOMENCLATURE_VALUES -> {
- val defaultMnemonicFilter = adapter?.defaultMnemonicFilter() ?: emptyList()
- val defaultNomenclatureValues = mutableListOf()
- data.moveToFirst()
-
- while (!data.isAfterLast) {
- val defaultNomenclatureValue = DefaultNomenclatureWithType.fromCursor(data)
-
- if (defaultNomenclatureValue != null && defaultMnemonicFilter.contains(
- defaultNomenclatureValue.nomenclatureWithType?.type?.mnemonic
- )
- ) {
- defaultNomenclatureValues.add(defaultNomenclatureValue)
- }
-
- data.moveToNext()
- }
-
- setPropertyValues(defaultNomenclatureValues)
- }
- }
- }
+ private lateinit var savedState: Bundle
- override fun onLoaderReset(loader: Loader) {
- when (loader.id) {
- LOADER_NOMENCLATURE_TYPES -> adapter?.bind(null)
- }
- }
- }
+ private var adapter: EditableNomenclatureTypeAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedState = savedInstanceState ?: Bundle()
+
+ with(nomenclatureViewModel) {
+ observe(
+ editableNomenclatures,
+ ::handleEditableNomenclatureTypes
+ )
+ }
}
override fun onCreateView(
@@ -147,68 +67,113 @@ class InformationFragment : Fragment(),
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- val recyclerView = inflater.inflate(
- R.layout.recycler_view,
+ return inflater.inflate(
+ R.layout.fragment_recycler_view_loader,
container,
false
)
- recyclerView.setPadding(resources.getDimensionPixelOffset(R.dimen.padding_default))
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(
+ view,
+ savedInstanceState
+ )
+
+ val recyclerView = view.findViewById(android.R.id.list)
+ val emptyTextView = view.findViewById(android.R.id.empty).apply {
+ text = getString(R.string.information_no_data)
+ }
+ val progressBar = view.findViewById(android.R.id.progress)
+ .apply { visibility = View.VISIBLE }
+
+ adapter = EditableNomenclatureTypeAdapter(object :
+ EditableNomenclatureTypeAdapter.OnEditableNomenclatureTypeAdapter {
+ override fun getLifecycleOwner(): LifecycleOwner {
+ return this@InformationFragment
+ }
+
+ override fun showEmptyTextView(show: Boolean) {
+ progressBar?.visibility = View.GONE
+
+ if (emptyTextView?.visibility == View.VISIBLE == show) {
+ return
+ }
+
+ if (show) {
+ emptyTextView?.startAnimation(
+ AnimationUtils.loadAnimation(
+ context,
+ android.R.anim.fade_in
+ )
+ )
+ emptyTextView?.visibility = View.VISIBLE
+ } else {
+ emptyTextView?.startAnimation(
+ AnimationUtils.loadAnimation(
+ context,
+ android.R.anim.fade_out
+ )
+ )
+ emptyTextView?.visibility = View.GONE
+ }
+ }
- // Set the adapter
- adapter = NomenclatureTypesRecyclerViewAdapter(object :
- NomenclatureTypesRecyclerViewAdapter.OnNomenclatureTypesRecyclerViewAdapterListener {
override fun showMore() {
savedState.putBoolean(
KEY_SHOW_ALL_NOMENCLATURE_TYPES,
true
)
- setPropertyValues()
}
- override fun onAction(nomenclatureTypeMnemonic: String) {
-
- val taxonomy = input?.getCurrentSelectedInputTaxon()?.taxon?.taxonomy
- ?: Taxonomy(
- Taxonomy.ANY,
- Taxonomy.ANY
- )
-
- val chooseNomenclatureDialogFragment = ChooseNomenclatureDialogFragment.newInstance(
+ override fun getNomenclatureValues(nomenclatureTypeMnemonic: String): LiveData> {
+ return nomenclatureViewModel.getNomenclatureValuesByTypeAndTaxonomy(
nomenclatureTypeMnemonic,
- taxonomy
- )
- chooseNomenclatureDialogFragment.show(
- childFragmentManager,
- CHOOSE_NOMENCLATURE_DIALOG_FRAGMENT
+ input?.getCurrentSelectedInputTaxon()?.taxon?.taxonomy
+ ?: Taxonomy(
+ Taxonomy.ANY,
+ Taxonomy.ANY
+ )
)
}
- override fun onEdit(
- nomenclatureTypeMnemonic: String,
- value: String?
- ) {
+ override fun onUpdate(editableNomenclatureType: EditableNomenclatureType) {
(input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.set(
- nomenclatureTypeMnemonic,
- PropertyValue.fromValue(
- nomenclatureTypeMnemonic,
- value
- )
+ editableNomenclatureType.code,
+ editableNomenclatureType.value
+ )
+
+ val propertyValue = editableNomenclatureType.value
+
+ if (propertyValue !== null && editableNomenclatureType.locked) propertyValueModel.setPropertyValue(
+ input?.getCurrentSelectedInputTaxon()?.taxon?.taxonomy
+ ?: Taxonomy(
+ Taxonomy.ANY,
+ Taxonomy.ANY
+ ),
+ propertyValue
+ ) else propertyValueModel.clearPropertyValue(
+ input?.getCurrentSelectedInputTaxon()?.taxon?.taxonomy
+ ?: Taxonomy(
+ Taxonomy.ANY,
+ Taxonomy.ANY
+ ),
+ editableNomenclatureType.code
)
}
})
- adapter?.showAllNomenclatureTypes(
- savedState.getBoolean(
+
+ if (savedState.getBoolean(
KEY_SHOW_ALL_NOMENCLATURE_TYPES,
false
)
- )
+ ) adapter?.showAllNomenclatureTypes() else adapter?.showDefaultNomenclatureTypes()
+ adapter?.lockDefaultValues(arguments?.getBoolean(ARG_SAVE_DEFAULT_VALUES) == true)
- with(recyclerView as RecyclerView) {
+ with(recyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = this@InformationFragment.adapter
}
-
- return recyclerView
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -232,76 +197,37 @@ class InformationFragment : Fragment(),
}
override fun refreshView() {
- LoaderManager.getInstance(this)
- .restartLoader(
- LOADER_NOMENCLATURE_TYPES,
- null,
- loaderCallbacks
- )
- }
-
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
- }
-
- override fun onSelectedNomenclature(
- nomenclatureType: String,
- nomenclature: Nomenclature
- ) {
-
- (input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.set(
- nomenclatureType,
- PropertyValue.fromNomenclature(
- nomenclatureType,
- nomenclature
- )
+ nomenclatureViewModel.getEditableNomenclatures(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ (arguments?.getParcelableArray(ARG_PROPERTIES)
+ ?.map { it as PropertySettings }
+ ?.toList() ?: emptyList()),
+ input?.getCurrentSelectedInputTaxon()?.taxon?.taxonomy
)
- setPropertyValues()
}
- private fun loadDefaultNomenclatureValues() {
- LoaderManager.getInstance(this)
- .restartLoader(
- LOADER_DEFAULT_NOMENCLATURE_VALUES,
- null,
- loaderCallbacks
- )
- }
-
- private fun setPropertyValues(defaultNomenclatureValues: List = emptyList()) {
- defaultNomenclatureValues.forEach {
- val nomenclatureType = it.nomenclatureWithType?.type?.mnemonic ?: return@forEach
-
- if ((input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.containsKey(
- nomenclatureType
- ) == true
- ) {
- return@forEach
- }
+ private fun handleEditableNomenclatureTypes(editableNomenclatureTypes: List) {
+ editableNomenclatureTypes.filter { it.value != null }.forEach {
+ if ((input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.containsKey(it.code) == true) return@forEach
(input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.set(
- nomenclatureType,
- PropertyValue.fromNomenclature(
- nomenclatureType,
- it.nomenclatureWithType
- )
+ it.code,
+ it.value
)
}
- adapter?.setPropertyValues(
- (input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.values?.toList()
- ?: emptyList()
+ adapter?.bind(
+ editableNomenclatureTypes,
+ *((input?.getCurrentSelectedInputTaxon() as InputTaxon?)?.properties?.values?.filterNotNull()
+ ?.toTypedArray()
+ ?: emptyArray())
)
}
companion object {
+ private const val ARG_SAVE_DEFAULT_VALUES = "arg_save_default_values"
private const val ARG_PROPERTIES = "arg_properties"
- private const val LOADER_NOMENCLATURE_TYPES = 1
- private const val LOADER_DEFAULT_NOMENCLATURE_VALUES = 2
- private const val CHOOSE_NOMENCLATURE_DIALOG_FRAGMENT =
- "choose_nomenclature_dialog_fragment"
-
private const val KEY_SHOW_ALL_NOMENCLATURE_TYPES = "show_all_nomenclature_types"
/**
@@ -310,8 +236,15 @@ class InformationFragment : Fragment(),
* @return A new instance of [InformationFragment]
*/
@JvmStatic
- fun newInstance(vararg propertySettings: PropertySettings) = InformationFragment().apply {
+ fun newInstance(
+ saveDefaultValues: Boolean = false,
+ vararg propertySettings: PropertySettings
+ ) = InformationFragment().apply {
arguments = Bundle().apply {
+ putBoolean(
+ ARG_SAVE_DEFAULT_VALUES,
+ saveDefaultValues
+ )
putParcelableArray(
ARG_PROPERTIES,
propertySettings
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/NomenclatureTypesRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/NomenclatureTypesRecyclerViewAdapter.kt
deleted file mode 100644
index 68206833..00000000
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/informations/NomenclatureTypesRecyclerViewAdapter.kt
+++ /dev/null
@@ -1,461 +0,0 @@
-package fr.geonature.occtax.ui.input.informations
-
-import android.database.Cursor
-import android.text.Editable
-import android.text.InputType
-import android.text.TextWatcher
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ArrayAdapter
-import android.widget.AutoCompleteTextView
-import android.widget.Button
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.textfield.TextInputLayout
-import fr.geonature.commons.data.entity.Nomenclature
-import fr.geonature.commons.data.entity.NomenclatureType
-import fr.geonature.commons.util.KeyboardUtils.hideSoftKeyboard
-import fr.geonature.occtax.R
-import fr.geonature.occtax.input.InputTaxon
-import fr.geonature.occtax.input.NomenclatureTypeViewType
-import fr.geonature.occtax.input.PropertyValue
-import fr.geonature.occtax.settings.PropertySettings
-import java.util.Locale
-
-/**
- * Default RecyclerView Adapter used by [InformationFragment].
- *
- * @author S. Grimault
- */
-class NomenclatureTypesRecyclerViewAdapter(private val listener: OnNomenclatureTypesRecyclerViewAdapterListener) :
- RecyclerView.Adapter() {
-
- private val mnemonicFilter = InputTaxon.defaultPropertiesMnemonic
- private val moreViewType = Triple(
- "MORE",
- NomenclatureTypeViewType.MORE,
- true
- )
-
- private val availableNomenclatureTypes =
- mutableListOf>()
- private val properties = mutableListOf()
- private var showAllNomenclatureTypes = false
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int
- ): AbstractViewHolder {
- return when (NomenclatureTypeViewType.values()[viewType]) {
- NomenclatureTypeViewType.MORE -> MoreViewHolder(parent)
- NomenclatureTypeViewType.TEXT_SIMPLE -> TextSimpleViewHolder(parent)
- NomenclatureTypeViewType.TEXT_MULTIPLE -> TextMultipleViewHolder(parent)
- else -> NomenclatureTypeViewHolder(parent)
- }
- }
-
- override fun getItemCount(): Int {
- return properties.size
- }
-
- override fun onBindViewHolder(
- holder: AbstractViewHolder,
- position: Int
- ) {
- holder.bind(properties[position])
- }
-
- override fun getItemViewType(position: Int): Int {
- val property = properties[position]
-
- return if (property.code == moreViewType.first) moreViewType.second.ordinal
- else mnemonicFilter.first { it.first == properties[position].code }
- .second.ordinal
- }
-
- fun defaultMnemonicFilter(): List {
- return mnemonicFilter.asSequence()
- .filter { it.second == NomenclatureTypeViewType.NOMENCLATURE_TYPE }
- .map { it.first }
- .toList()
- }
-
- fun bind(cursor: Cursor?, vararg defaultPropertySettings: PropertySettings) {
- availableNomenclatureTypes.clear()
-
- cursor?.run {
- if (this.isClosed) return@run
-
- this.moveToFirst()
-
- while (!this.isAfterLast) {
- NomenclatureType.fromCursor(this)
- ?.run {
- (if (defaultPropertySettings.isEmpty()) {
- mnemonicFilter.find { it.first == mnemonic }
- } else {
- defaultPropertySettings.find { it.key == mnemonic && it.visible }
- ?.let { property -> mnemonicFilter.find { it.first == property.key } }
- })?.also {
- availableNomenclatureTypes.add(it)
- }
- }
- cursor.moveToNext()
- }
-
- // add default mnemonic filters
- availableNomenclatureTypes.addAll(mnemonicFilter.filter {
- it.second != NomenclatureTypeViewType.NOMENCLATURE_TYPE &&
- (defaultPropertySettings.isEmpty() || defaultPropertySettings.any { p -> p.key == it.first })
- })
- }
-
- availableNomenclatureTypes.sortWith { o1, o2 ->
- val i1 = mnemonicFilter.indexOfFirst { it.first == o1.first }
- val i2 = mnemonicFilter.indexOfFirst { it.first == o2.first }
-
- when {
- i1 == -1 -> 1
- i2 == -1 -> -1
- else -> i1 - i2
- }
- }
-
- val nomenclatureTypes = if (showAllNomenclatureTypes) {
- availableNomenclatureTypes
- } else {
- val defaultNomenclatureTypes =
- availableNomenclatureTypes.filter { availableNomenclatureType ->
- if (defaultPropertySettings.isEmpty()) availableNomenclatureType.third
- else defaultPropertySettings.any { it.key == availableNomenclatureType.first && it.default }
- }
-
- // add MORE ViewType if default nomenclature types are presents
- if (defaultNomenclatureTypes.size < availableNomenclatureTypes.size) {
- listOf(
- *defaultNomenclatureTypes.toTypedArray(),
- moreViewType
- )
- } else {
- availableNomenclatureTypes
- }
- }
-
- setNomenclatureTypes(nomenclatureTypes)
- }
-
- fun setPropertyValues(selectedProperties: List) {
- val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
- override fun getOldListSize(): Int {
- return this@NomenclatureTypesRecyclerViewAdapter.properties.size
- }
-
- override fun getNewListSize(): Int {
- return this@NomenclatureTypesRecyclerViewAdapter.properties.size
- }
-
- override fun areItemsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ): Boolean {
- return true
- }
-
- override fun areContentsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ): Boolean {
- val oldProperty =
- this@NomenclatureTypesRecyclerViewAdapter.properties[oldItemPosition]
- val newProperty = selectedProperties.firstOrNull { it.code == oldProperty.code }
-
- return oldProperty == newProperty
- }
- })
-
- val newProperties = this.properties.map { p ->
- selectedProperties.firstOrNull { it.code == p.code } ?: p
- }
- this.properties.clear()
- this.properties.addAll(newProperties)
-
- diffResult.dispatchUpdatesTo(this)
- }
-
- fun showAllNomenclatureTypes(showAllNomenclatureTypes: Boolean = false) {
- this.showAllNomenclatureTypes = showAllNomenclatureTypes
- setNomenclatureTypes(availableNomenclatureTypes)
- }
-
- private fun setNomenclatureTypes(nomenclatureTypes: List>) {
- if (this.properties.isEmpty()) {
- this.properties.addAll(nomenclatureTypes.map {
- when (it.second) {
- NomenclatureTypeViewType.NOMENCLATURE_TYPE -> PropertyValue.fromNomenclature(
- it.first,
- null
- )
- else -> PropertyValue.fromValue(
- it.first,
- null
- )
- }
- })
-
- if (this.properties.isNotEmpty()) {
- notifyItemRangeInserted(
- 0,
- this.properties.size
- )
- }
-
- return
- }
-
- if (nomenclatureTypes.isEmpty()) {
- val numberOfProperties = this.properties.size
- this.properties.clear()
- notifyItemRangeRemoved(
- 0,
- numberOfProperties
- )
-
- return
- }
-
- val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
-
- override fun getOldListSize(): Int {
- return this@NomenclatureTypesRecyclerViewAdapter.properties.size
- }
-
- override fun getNewListSize(): Int {
- return nomenclatureTypes.size
- }
-
- override fun areItemsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ): Boolean {
- return this@NomenclatureTypesRecyclerViewAdapter.properties[oldItemPosition].code == nomenclatureTypes[newItemPosition].first
- }
-
- override fun areContentsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ): Boolean {
- return this@NomenclatureTypesRecyclerViewAdapter.properties[oldItemPosition].code == nomenclatureTypes[newItemPosition].first
- }
- })
-
- val newProperties = nomenclatureTypes.map { pair ->
- properties.firstOrNull {
- it.code == pair.first
- }
- ?: when (pair.second) {
- NomenclatureTypeViewType.NOMENCLATURE_TYPE -> PropertyValue.fromNomenclature(
- pair.first,
- null
- )
- else -> PropertyValue.fromValue(
- pair.first,
- null
- )
- }
- }
-
- this.properties.clear()
- this.properties.addAll(newProperties)
-
- diffResult.dispatchUpdatesTo(this)
- }
-
- abstract inner class AbstractViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- internal var property: PropertyValue? = null
-
- fun bind(property: PropertyValue) {
- this.property = property
-
- onBind(property)
- }
-
- abstract fun onBind(property: PropertyValue)
-
- fun getNomenclatureTypeLabel(mnemonic: String): String {
- val resourceId = itemView.resources.getIdentifier(
- "nomenclature_${mnemonic.lowercase(Locale.getDefault())}",
- "string",
- itemView.context.packageName
- )
-
- return if (resourceId == 0) mnemonic else itemView.context.getString(resourceId)
- }
- }
-
- inner class NomenclatureTypeViewHolder(parent: ViewGroup) : AbstractViewHolder(
- LayoutInflater.from(parent.context).inflate(
- R.layout.view_action_nomenclature_type_select,
- parent,
- false
- )
- ) {
- private var edit: TextInputLayout = itemView.findViewById(android.R.id.edit)
-
- init {
- (edit.editText as? AutoCompleteTextView)?.setAdapter(
- ArrayAdapter(
- parent.context,
- R.layout.list_item_2
- )
- )
- }
-
- override fun onBind(property: PropertyValue) {
- with(edit) {
- hint = getNomenclatureTypeLabel(property.code)
- setEndIconOnClickListener {
- listener.onAction(property.code)
- }
-
- editText?.apply {
- setOnClickListener {
- listener.onAction(property.code)
- }
- text = property.label?.let {
- Editable.Factory
- .getInstance()
- .newEditable(it)
- }
- }
- }
- }
- }
-
- inner class MoreViewHolder(parent: ViewGroup) : AbstractViewHolder(
- LayoutInflater.from(parent.context).inflate(
- R.layout.view_action_more,
- parent,
- false
- )
- ) {
- private var button1: Button = itemView.findViewById(android.R.id.button1)
-
- override fun onBind(property: PropertyValue) {
- with(button1) {
- text = getNomenclatureTypeLabel(property.code)
- setOnClickListener {
- showAllNomenclatureTypes(true)
- listener.showMore()
- }
- }
- }
- }
-
- open inner class TextSimpleViewHolder(parent: ViewGroup) : AbstractViewHolder(
- LayoutInflater.from(parent.context).inflate(
- R.layout.view_action_edit_text,
- parent,
- false
- )
- ) {
- internal var edit: TextInputLayout = itemView.findViewById(android.R.id.edit)
- private val textWatcher = object : TextWatcher {
- override fun beforeTextChanged(
- s: CharSequence?,
- start: Int,
- count: Int,
- after: Int
- ) {
- }
-
- override fun onTextChanged(
- s: CharSequence?,
- start: Int,
- before: Int,
- count: Int
- ) {
- }
-
- override fun afterTextChanged(s: Editable?) {
- val property = property ?: return
-
- listener.onEdit(property.code,
- s?.toString()?.ifEmpty { null }?.ifBlank { null })
- }
- }
-
- init {
- with(edit) {
- editText?.addTextChangedListener(textWatcher)
- setOnFocusChangeListener { v, hasFocus ->
- if (!hasFocus) {
- // workaround to force hide the soft keyboard
- hideSoftKeyboard(v)
- }
- }
- }
- }
-
- override fun onBind(property: PropertyValue) {
- edit.hint = getEditTextHint(property.code)
-
- if (property.value is String? && !property.value.isNullOrEmpty()) {
- edit.editText?.removeTextChangedListener(textWatcher)
- edit.editText?.text =
- property.value?.let { Editable.Factory.getInstance().newEditable(it) }
- edit.editText?.addTextChangedListener(textWatcher)
- }
- }
-
- private fun getEditTextHint(mnemonic: String): String {
- val resourceId = itemView.resources.getIdentifier(
- "information_${mnemonic.lowercase(Locale.getDefault())}_hint",
- "string",
- itemView.context.packageName
- )
- return if (resourceId == 0) "" else itemView.context.getString(resourceId)
- }
- }
-
- inner class TextMultipleViewHolder(parent: ViewGroup) : TextSimpleViewHolder(parent) {
- init {
- edit.isCounterEnabled = true
- edit.editText?.apply {
- isSingleLine = false
- inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
- minLines = 2
- maxLines = 4
- }
- }
- }
-
- /**
- * Callback used by [NomenclatureTypesRecyclerViewAdapter].
- */
- interface OnNomenclatureTypesRecyclerViewAdapterListener {
-
- /**
- * Called when the 'more' action button has been clicked.
- */
- fun showMore()
-
- /**
- * Called when the action button has been clicked for a given nomenclature type.
- *
- * @param nomenclatureTypeMnemonic the selected nomenclature type
- */
- fun onAction(nomenclatureTypeMnemonic: String)
-
- /**
- * Called when a value has been directly edited for a given nomenclature type.
- *
- * @param nomenclatureTypeMnemonic the selected nomenclature type
- * @param value the corresponding value (may be `null`)
- */
- fun onEdit(
- nomenclatureTypeMnemonic: String,
- value: String?
- )
- }
-}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt
index d5ec9fc2..8765a5a5 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/map/InputMapFragment.kt
@@ -1,8 +1,10 @@
package fr.geonature.occtax.ui.input.map
+import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
-import fr.geonature.commons.input.AbstractInput
+import androidx.fragment.app.activityViewModels
+import fr.geonature.commons.lifecycle.observeUntil
import fr.geonature.commons.util.ThemeUtils
import fr.geonature.maps.jts.geojson.GeometryUtils.fromPoint
import fr.geonature.maps.jts.geojson.GeometryUtils.toPoint
@@ -14,9 +16,10 @@ import fr.geonature.maps.ui.overlay.feature.filter.ContainsFeaturesFilter
import fr.geonature.maps.ui.widget.EditFeatureButton
import fr.geonature.occtax.R
import fr.geonature.occtax.input.Input
+import fr.geonature.occtax.input.InputViewModel
import fr.geonature.occtax.ui.input.IInputFragment
-import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
+import fr.geonature.viewpager.model.IPageWithValidationFragment
+import fr.geonature.viewpager.ui.OnPageFragmentListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
@@ -30,9 +33,13 @@ import org.osmdroid.views.MapView
* @author S. Grimault
*/
class InputMapFragment : MapFragment(),
- IValidateFragment,
+ IPageWithValidationFragment,
IInputFragment {
+ private val inputViewModel: InputViewModel by activityViewModels()
+
+ private lateinit var listener: OnPageFragmentListener
+
private var input: Input? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -56,6 +63,29 @@ class InputMapFragment : MapFragment(),
}
}
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ if (context is OnPageFragmentListener) {
+ listener = context
+ } else {
+ throw RuntimeException("$context must implement ${OnPageFragmentListener::class.simpleName}")
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ inputViewModel.input.observeUntil(
+ viewLifecycleOwner,
+ { it != null }) {
+ if (it == null) return@observeUntil
+
+ input = it
+ refreshView()
+ }
+ }
+
override fun onPause() {
super.onPause()
@@ -86,14 +116,10 @@ class InputMapFragment : MapFragment(),
}
}
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
- }
-
private fun clearInputSelection() {
input?.geometry = null
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
CoroutineScope(Main).launch {
getOverlays { overlay -> overlay is FeatureCollectionOverlay }
@@ -131,7 +157,7 @@ class InputMapFragment : MapFragment(),
.map { it.id }
.firstOrNull()
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt
index 553d5d47..25baf20c 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/observers/ObserversAndDateInputFragment.kt
@@ -5,29 +5,22 @@ import android.content.Intent
import android.database.Cursor
import android.os.Bundle
import android.text.Editable
-import android.text.format.DateFormat
-import android.text.format.DateFormat.is24HourFormat
import android.util.Pair
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.EditText
import android.widget.ListView
+import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
-import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.viewModels
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
-import com.google.android.material.datepicker.CalendarConstraints
-import com.google.android.material.datepicker.DateValidatorPointBackward
-import com.google.android.material.datepicker.DateValidatorPointForward
-import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.textfield.TextInputLayout
-import com.google.android.material.timepicker.MaterialTimePicker
-import com.google.android.material.timepicker.TimeFormat
import dagger.hilt.android.AndroidEntryPoint
import fr.geonature.commons.data.ContentProviderAuthority
import fr.geonature.commons.data.GeoNatureModuleName
@@ -37,44 +30,34 @@ import fr.geonature.commons.data.entity.DefaultNomenclatureWithType
import fr.geonature.commons.data.entity.InputObserver
import fr.geonature.commons.data.entity.NomenclatureType
import fr.geonature.commons.data.helper.ProviderHelper.buildUri
-import fr.geonature.commons.input.AbstractInput
import fr.geonature.commons.util.afterTextChanged
-import fr.geonature.commons.util.get
-import fr.geonature.commons.util.set
import fr.geonature.occtax.R
+import fr.geonature.occtax.features.nomenclature.presentation.PropertyValueModel
import fr.geonature.occtax.input.Input
import fr.geonature.occtax.input.NomenclatureTypeViewType
import fr.geonature.occtax.input.PropertyValue
import fr.geonature.occtax.settings.InputDateSettings
import fr.geonature.occtax.ui.dataset.DatasetListActivity
-import fr.geonature.occtax.ui.input.IInputFragment
+import fr.geonature.occtax.ui.input.AbstractInputFragment
import fr.geonature.occtax.ui.input.InputPagerFragmentActivity
import fr.geonature.occtax.ui.observers.InputObserverListActivity
+import fr.geonature.occtax.ui.shared.view.ActionView
+import fr.geonature.occtax.ui.shared.view.InputDateView
import fr.geonature.occtax.ui.shared.view.ListItemActionView
import fr.geonature.occtax.util.SettingsUtils.getDefaultDatasetId
import fr.geonature.occtax.util.SettingsUtils.getDefaultObserversId
-import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import org.tinylog.kotlin.Logger
-import java.util.Calendar
import java.util.Date
import java.util.Locale
import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
/**
- * Selected observer and current date as first {@code Fragment} used by [InputPagerFragmentActivity].
+ * Selected observer and current date as first page used by [InputPagerFragmentActivity].
*
* @author S. Grimault
*/
@AndroidEntryPoint
-class ObserversAndDateInputFragment : Fragment(),
- IValidateFragment,
- IInputFragment {
+class ObserversAndDateInputFragment : AbstractInputFragment() {
@ContentProviderAuthority
@Inject
@@ -84,19 +67,19 @@ class ObserversAndDateInputFragment : Fragment(),
@Inject
lateinit var moduleName: String
+ private val propertyValueModel: PropertyValueModel by viewModels()
+
private lateinit var observersResultLauncher: ActivityResultLauncher
private lateinit var datasetResultLauncher: ActivityResultLauncher
private lateinit var dateSettings: InputDateSettings
- private var input: Input? = null
private val defaultInputObservers: MutableList = mutableListOf()
private val selectedInputObservers: MutableList = mutableListOf()
private var selectedDataset: Dataset? = null
private var selectedInputObserversActionView: ListItemActionView? = null
- private var selectedDatasetActionView: ListItemActionView? = null
- private var dateStartTextInputLayout: TextInputLayout? = null
- private var dateEndTextInputLayout: TextInputLayout? = null
+ private var selectedDatasetActionView: ActionView? = null
+ private var inputDateView: InputDateView? = null
private var commentTextInputLayout: TextInputLayout? = null
private val loaderCallbacks = object : LoaderManager.LoaderCallbacks {
@@ -197,7 +180,7 @@ class ObserversAndDateInputFragment : Fragment(),
if (data.count == 0) {
selectedDataset = null
input?.datasetId = null
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
if (data.moveToFirst()) {
@@ -235,7 +218,7 @@ class ObserversAndDateInputFragment : Fragment(),
data.moveToNext()
}
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
if (input?.properties?.isNotEmpty() == false) {
val context = context ?: return
@@ -322,8 +305,8 @@ class ObserversAndDateInputFragment : Fragment(),
}
selectedDatasetActionView =
- view.findViewById(R.id.selected_dataset_action_view)?.apply {
- setListener(object : ListItemActionView.OnListItemActionViewListener {
+ view.findViewById(R.id.selected_dataset_action_view)?.apply {
+ setListener(object : ActionView.OnActionViewListener {
override fun onAction() {
val context = context ?: return
@@ -337,105 +320,24 @@ class ObserversAndDateInputFragment : Fragment(),
})
}
- dateStartTextInputLayout = view.findViewById(R.id.dateStart)?.apply {
- hint = getString(
- if (dateSettings.endDateSettings == null) R.string.observers_and_date_date_hint
- else R.string.observers_and_date_date_start_hint
- )
- editText?.afterTextChanged {
- error = checkStartDateConstraints()
- dateEndTextInputLayout?.error = checkEndDateConstraints()
- }
- editText?.setOnClickListener {
- CoroutineScope(Dispatchers.Main).launch {
- val startDate = selectDateTime(
- CalendarConstraints
- .Builder()
- .setValidator(DateValidatorPointBackward.now())
- .build(),
- dateSettings.startDateSettings == InputDateSettings.DateSettings.DATETIME,
- input?.startDate ?: Date()
- )
+ inputDateView = view.findViewById(R.id.input_date)?.apply {
+ setInputDateSettings(dateSettings)
+ setListener(object : InputDateView.OnInputDateViewListener {
+ override fun fragmentManager(): FragmentManager? {
+ return activity?.supportFragmentManager
+ }
+ override fun onDatesChanged(startDate: Date, endDate: Date) {
input?.startDate = startDate
+ input?.endDate = endDate
- if (dateSettings.endDateSettings == null) {
- input?.endDate = startDate
- }
-
- dateStartTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
- startDate
- )
- }
- dateEndTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
- input?.endDate
- )
- }
-
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
- }
- }
-
- dateEndTextInputLayout = view.findViewById(R.id.dateEnd)?.apply {
- visibility = if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE
- editText?.afterTextChanged {
- error = checkEndDateConstraints()
- dateStartTextInputLayout?.error = checkStartDateConstraints()
- }
- editText?.setOnClickListener {
- CoroutineScope(Dispatchers.Main).launch {
- val endDate = selectDateTime(
- CalendarConstraints
- .Builder()
- .setValidator(
- DateValidatorPointForward.from(
- (input?.startDate ?: Date())
- .set(
- Calendar.HOUR_OF_DAY,
- 0
- ).set(
- Calendar.MINUTE,
- 0
- ).set(
- Calendar.SECOND,
- 0
- ).set(
- Calendar.MILLISECOND,
- 0
- ).time
- )
- )
- .build(),
- dateSettings.endDateSettings == InputDateSettings.DateSettings.DATETIME,
- input?.endDate ?: input?.startDate ?: Date()
- )
-
- input?.endDate = endDate
- dateStartTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
- input?.startDate
- )
- }
- dateEndTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
- endDate
- )
- }
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ override fun hasError(message: CharSequence) {
+ listener.validateCurrentPage()
}
- }
+ })
}
commentTextInputLayout = view.findViewById(android.R.id.edit)?.apply {
@@ -464,31 +366,37 @@ class ObserversAndDateInputFragment : Fragment(),
?.isNotEmpty() ?: false &&
this.input?.datasetId != null &&
this.input?.properties?.isNotEmpty() == true &&
- checkStartDateConstraints() == null &&
- checkEndDateConstraints() == null
+ inputDateView?.hasErrors() == false
}
override fun refreshView() {
+ // clear any existing local property default values
+ if (arguments?.getBoolean(ARG_SAVE_DEFAULT_VALUES) == false) {
+ propertyValueModel.clearAllPropertyValues()
+ }
+
setDefaultDatasetFromSettings()
- val selectedInputObserverIds =
- input?.getAllInputObserverIds() ?: context?.let { getDefaultObserversId(it) }
- ?: emptyList()
+ input?.getAllInputObserverIds()?.also { selectedInputObserverIdsFromInput ->
+ val selectedInputObserverIds = selectedInputObserverIdsFromInput.ifEmpty {
+ context?.let { getDefaultObserversId(it) } ?: emptyList()
+ }
- if (selectedInputObserverIds.isNotEmpty()) {
- LoaderManager.getInstance(this)
- .initLoader(
- LOADER_OBSERVERS_IDS,
- bundleOf(
- kotlin.Pair(
- KEY_SELECTED_INPUT_OBSERVER_IDS,
- selectedInputObserverIds.toTypedArray().toLongArray()
- )
- ),
- loaderCallbacks
- )
- } else {
- updateSelectedObserversActionView(emptyList())
+ if (selectedInputObserverIds.isNotEmpty()) {
+ LoaderManager.getInstance(this)
+ .initLoader(
+ LOADER_OBSERVERS_IDS,
+ bundleOf(
+ kotlin.Pair(
+ KEY_SELECTED_INPUT_OBSERVER_IDS,
+ selectedInputObserverIds.toTypedArray().toLongArray()
+ )
+ ),
+ loaderCallbacks
+ )
+ } else {
+ updateSelectedObserversActionView(emptyList())
+ }
}
val selectedDatasetId = input?.datasetId
@@ -525,32 +433,21 @@ class ObserversAndDateInputFragment : Fragment(),
loaderCallbacks
)
- dateStartTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
- input?.startDate ?: Date()
- )
- }
- dateEndTextInputLayout?.editText?.apply {
- updateDateEditText(
- this,
- dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
- input?.endDate
- )
- }
- commentTextInputLayout?.hint =
- getString(
- if (input?.comment.isNullOrBlank()) R.string.observers_and_date_comment_add_hint
- else R.string.observers_and_date_comment_edit_hint
- )
- commentTextInputLayout?.editText?.apply {
- text = input?.comment?.let { Editable.Factory.getInstance().newEditable(it) }
- }
- }
+ inputDateView?.setDates(
+ startDate = input?.startDate ?: Date(),
+ endDate = input?.endDate
+ )
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
+ commentTextInputLayout?.apply {
+ hint =
+ getString(
+ if (input?.comment.isNullOrBlank()) R.string.input_comment_add_hint
+ else R.string.input_comment_edit_hint
+ )
+ editText?.apply {
+ text = input?.comment?.let { Editable.Factory.getInstance().newEditable(it) }
+ }
+ }
}
private fun updateSelectedObservers(selectedInputObservers: List) {
@@ -564,7 +461,7 @@ class ObserversAndDateInputFragment : Fragment(),
updateSelectedObserversActionView(selectedInputObservers)
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
private fun updateSelectedDataset(selectedDataset: Dataset?) {
@@ -574,7 +471,7 @@ class ObserversAndDateInputFragment : Fragment(),
it.datasetId = selectedDataset?.id
}
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
updateSelectedDatasetActionView(selectedDataset)
}
@@ -596,15 +493,13 @@ class ObserversAndDateInputFragment : Fragment(),
}
private fun updateSelectedDatasetActionView(selectedDataset: Dataset?) {
- selectedDatasetActionView?.setItems(
- if (selectedDataset == null) emptyList()
- else listOf(
- Pair.create(
- selectedDataset.name,
- selectedDataset.description
- )
- )
- )
+ selectedDatasetActionView?.getContentView()?.also { contentView ->
+ contentView.isSelected = true
+ contentView.findViewById(R.id.dataset_name)?.text = selectedDataset?.name
+ contentView.findViewById(R.id.dataset_description)?.text =
+ selectedDataset?.description
+ }
+ selectedDatasetActionView?.setContentViewVisibility(if (selectedDataset == null) View.GONE else View.VISIBLE)
}
private fun setDefaultDatasetFromSettings() {
@@ -618,171 +513,11 @@ class ObserversAndDateInputFragment : Fragment(),
}
}
- /**
- * Select a new date from given optional date through date/time pickers.
- * If no date was given, use the current date.
- */
- private suspend fun selectDateTime(
- bounds: CalendarConstraints,
- withTime: Boolean = false,
- from: Date = Date()
- ): Date =
- suspendCoroutine { continuation ->
- val supportFragmentManager =
- activity?.supportFragmentManager
-
- if (supportFragmentManager == null) {
- continuation.resume(from)
-
- return@suspendCoroutine
- }
-
- val context = context
-
- if (context == null) {
- continuation.resume(from)
-
- return@suspendCoroutine
- }
-
- with(
- MaterialDatePicker.Builder
- .datePicker()
- .setSelection(from.time)
- .setCalendarConstraints(bounds)
- .build()
- ) {
- addOnPositiveButtonClickListener {
- val selectedDate = Date(it).set(
- Calendar.HOUR_OF_DAY,
- from.get(Calendar.HOUR_OF_DAY)
- ).set(
- Calendar.MINUTE,
- from.get(Calendar.MINUTE)
- )
-
- if (!withTime) {
- continuation.resume(selectedDate)
-
- return@addOnPositiveButtonClickListener
- }
-
- with(
- MaterialTimePicker.Builder()
- .setTimeFormat(if (is24HourFormat(context)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H)
- .setHour(selectedDate.get(if (is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR))
- .setMinute(selectedDate.get(Calendar.MINUTE))
- .build()
- ) {
- addOnPositiveButtonClickListener {
- continuation.resume(
- selectedDate.set(
- if (is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR,
- hour
- ).set(
- Calendar.MINUTE,
- minute
- )
- )
- }
- addOnNegativeButtonClickListener {
- continuation.resume(selectedDate)
- }
- addOnCancelListener {
- continuation.resume(selectedDate)
- }
- show(
- supportFragmentManager,
- TIME_PICKER_DIALOG_FRAGMENT
- )
- }
- }
- addOnNegativeButtonClickListener {
- continuation.resume(from)
- }
- addOnCancelListener {
- continuation.resume(from)
- }
- show(
- supportFragmentManager,
- DATE_PICKER_DIALOG_FRAGMENT
- )
- }
- }
-
- private fun updateDateEditText(
- editText: EditText,
- dateSettings: InputDateSettings.DateSettings,
- date: Date?
- ) {
- editText.text = date?.let {
- Editable.Factory
- .getInstance()
- .newEditable(
- DateFormat.format(
- getString(
- if (dateSettings == InputDateSettings.DateSettings.DATETIME) R.string.observers_and_date_datetime_format
- else R.string.observers_and_date_date_format
- ),
- it
- ).toString()
- )
- }
- }
-
- /**
- * Checks start date constraints from current [AbstractInput].
- *
- * @return `null` if all constraints are valid, or an error message
- */
- private fun checkStartDateConstraints(): CharSequence? {
- if (input == null) {
- return null
- }
-
- val startDate = input?.startDate
- ?: return getString(R.string.observers_and_date_error_date_start_not_set)
-
- if (startDate.after(Date())) {
- return getString(R.string.observers_and_date_error_date_start_after_now)
- }
-
- return null
- }
-
- /**
- * Checks end date constraints from current [AbstractInput].
- *
- * @return `null` if all constraints are valid, or an error message
- */
- private fun checkEndDateConstraints(): CharSequence? {
- if (input == null) {
- return null
- }
-
- val endDate = input?.endDate
-
- if (dateSettings.endDateSettings == null) {
- return null
- }
-
- if (endDate == null) {
- return getString(R.string.observers_and_date_error_date_end_not_set)
- }
-
- if ((input?.startDate ?: Date()).after(endDate)) {
- return getString(R.string.observers_and_date_error_date_end_before_start_date)
- }
-
- return null
- }
-
companion object {
private const val ARG_DATE_SETTINGS = "arg_date_settings"
+ private const val ARG_SAVE_DEFAULT_VALUES = "arg_save_default_values"
- private const val DATE_PICKER_DIALOG_FRAGMENT = "date_picker_dialog_fragment"
- private const val TIME_PICKER_DIALOG_FRAGMENT = "time_picker_dialog_fragment"
private const val LOADER_OBSERVERS_IDS = 1
private const val LOADER_DATASET_ID = 2
private const val LOADER_DEFAULT_NOMENCLATURE_VALUES = 3
@@ -794,13 +529,18 @@ class ObserversAndDateInputFragment : Fragment(),
* @return A new instance of [ObserversAndDateInputFragment]
*/
@JvmStatic
- fun newInstance(dateSettings: InputDateSettings) = ObserversAndDateInputFragment().apply {
- arguments = Bundle().apply {
- putParcelable(
- ARG_DATE_SETTINGS,
- dateSettings
- )
+ fun newInstance(dateSettings: InputDateSettings, saveDefaultValues: Boolean = false) =
+ ObserversAndDateInputFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(
+ ARG_DATE_SETTINGS,
+ dateSettings
+ )
+ putBoolean(
+ ARG_SAVE_DEFAULT_VALUES,
+ saveDefaultValues
+ )
+ }
}
- }
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt
index 9512d9ce..ca8810dc 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryFragment.kt
@@ -1,9 +1,12 @@
package fr.geonature.occtax.ui.input.summary
+import android.app.Activity
+import android.content.Intent
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
-import android.text.TextUtils
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -11,56 +14,81 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
-import android.widget.TextView
+import android.widget.ProgressBar
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
-import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat.getSystemService
-import androidx.fragment.app.Fragment
+import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
-import fr.geonature.commons.input.AbstractInput
+import fr.geonature.commons.data.entity.Taxonomy
import fr.geonature.commons.input.AbstractInputTaxon
import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter
import fr.geonature.occtax.R
-import fr.geonature.occtax.input.Input
-import fr.geonature.occtax.ui.input.IInputFragment
-import fr.geonature.occtax.ui.shared.dialog.CommentDialogFragment
-import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
+import fr.geonature.occtax.input.InputTaxon
+import fr.geonature.occtax.settings.InputDateSettings
+import fr.geonature.occtax.ui.input.AbstractInputFragment
+import fr.geonature.occtax.ui.input.taxa.Filter
+import fr.geonature.occtax.ui.input.taxa.FilterTaxonomy
+import fr.geonature.occtax.ui.input.taxa.TaxaFilterActivity
+import fr.geonature.occtax.ui.shared.dialog.InputDateDialogFragment
+import java.util.Date
/**
* Summary of all edited taxa.
*
* @author S. Grimault
*/
-class InputTaxaSummaryFragment : Fragment(),
- IValidateFragment,
- IInputFragment {
+class InputTaxaSummaryFragment : AbstractInputFragment() {
+
+ private lateinit var savedState: Bundle
+ private lateinit var dateSettings: InputDateSettings
+ private lateinit var taxaFilterResultLauncher: ActivityResultLauncher
- private var input: Input? = null
private var adapter: InputTaxaSummaryRecyclerViewAdapter? = null
- private var recyclerView: RecyclerView? = null
- private var emptyTextView: TextView? = null
- private var fab: ExtendedFloatingActionButton? = null
-
- private val onCommentDialogFragmentListener =
- object : CommentDialogFragment.OnCommentDialogFragmentListener {
- override fun onChanged(comment: String?) {
- input?.comment = comment
- activity?.invalidateOptionsMenu()
+ private var progressBar: ProgressBar? = null
+ private var emptyTextView: View? = null
+ private var filterChipGroup: ChipGroup? = null
+ private var startEditTaxon = false
+
+ private val onInputDateDialogFragmentListener =
+ object : InputDateDialogFragment.OnInputDateDialogFragmentListener {
+ override fun onDatesChanged(startDate: Date, endDate: Date) {
+ input?.apply {
+ this.startDate = startDate
+ this.endDate = endDate
+ }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ savedState = savedInstanceState ?: Bundle()
+ dateSettings = arguments?.getParcelable(ARG_DATE_SETTINGS) ?: InputDateSettings.DEFAULT
+
val supportFragmentManager = activity?.supportFragmentManager ?: return
- (supportFragmentManager.findFragmentByTag(COMMENT_DIALOG_FRAGMENT) as CommentDialogFragment?)?.also {
- it.setOnCommentDialogFragmentListener(onCommentDialogFragmentListener)
+ (supportFragmentManager.findFragmentByTag(INPUT_DATE_DIALOG_FRAGMENT) as InputDateDialogFragment?)?.also {
+ it.setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener)
}
+
+ taxaFilterResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
+ if ((activityResult.resultCode != Activity.RESULT_OK) || (activityResult.data == null)) {
+ return@registerForActivityResult
+ }
+
+ val selectedFilters =
+ activityResult.data?.getParcelableArrayExtra(TaxaFilterActivity.EXTRA_SELECTED_FILTERS)
+ ?.map { it as Filter<*> }?.toTypedArray() ?: emptyArray()
+ applyFilters(*selectedFilters)
+ }
}
override fun onCreateView(
@@ -69,7 +97,7 @@ class InputTaxaSummaryFragment : Fragment(),
savedInstanceState: Bundle?
): View? {
return inflater.inflate(
- R.layout.fragment_recycler_view_fab,
+ R.layout.fragment_input_summary,
container,
false
)
@@ -87,29 +115,25 @@ class InputTaxaSummaryFragment : Fragment(),
// we have a menu item to show in action bar
setHasOptionsMenu(true)
- recyclerView = view.findViewById(android.R.id.list)
- fab = view.findViewById(R.id.fab)
-
+ val recyclerView = view.findViewById(android.R.id.list)
+ progressBar = view.findViewById(android.R.id.progress)
emptyTextView = view.findViewById(android.R.id.empty)
- emptyTextView?.text = getString(R.string.summary_no_data)
+ filterChipGroup = view.findViewById(R.id.chip_group_filter)
- fab?.apply {
- setText(R.string.action_add_taxon)
- extend()
+ view.findViewById(R.id.fab).apply {
setOnClickListener {
- ((activity as AbstractPagerFragmentActivity?))?.also {
- input?.clearCurrentSelectedInputTaxon()
- it.goToPreviousPage()
- it.goToNextPage()
- }
+ startEditTaxon = true
+ input?.clearCurrentSelectedInputTaxon()
+ listener.startEditTaxon()
}
}
adapter = InputTaxaSummaryRecyclerViewAdapter(object :
AbstractListItemRecyclerViewAdapter.OnListItemRecyclerViewAdapterListener {
override fun onClick(item: AbstractInputTaxon) {
+ startEditTaxon = true
input?.setCurrentSelectedInputTaxonId(item.taxon.id)
- (activity as AbstractPagerFragmentActivity?)?.goToPageByKey(R.string.pager_fragment_information_title)
+ listener.startEditTaxon()
}
override fun onLongClicked(
@@ -134,7 +158,7 @@ class InputTaxaSummaryFragment : Fragment(),
) { dialog, _ ->
adapter?.remove(item)
input?.removeInputTaxon(item.taxon.id)
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
dialog.dismiss()
}
@@ -182,6 +206,34 @@ class InputTaxaSummaryFragment : Fragment(),
}
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(savedState.apply { putAll(outState) })
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ Handler(Looper.getMainLooper()).post {
+ // bypass this page and redirect to the previous one if we have started editing the first taxon
+ if (startEditTaxon && input?.getInputTaxa()?.isEmpty() == true) {
+ startEditTaxon = false
+ listener.goToPreviousPage()
+ return@post
+ }
+
+ // no taxon added yet: redirect to the edit taxon pages
+ if (input?.getInputTaxa()?.isEmpty() == true) {
+ startEditTaxon = true
+ listener.startEditTaxon()
+ return@post
+ }
+
+ // finish taxon editing workflow
+ startEditTaxon = false
+ listener.finishEditTaxon()
+ }
+ }
+
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater
@@ -192,38 +244,57 @@ class InputTaxaSummaryFragment : Fragment(),
inflater
)
- inflater.inflate(
- R.menu.comment,
- menu
- )
+ with(inflater) {
+ inflate(
+ R.menu.date,
+ menu
+ )
+ inflate(
+ R.menu.filter,
+ menu
+ )
+ }
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
- val commentItem = menu.findItem(R.id.menu_comment)
- commentItem.title =
- if (TextUtils.isEmpty(input?.comment)) getString(R.string.action_comment_add) else getString(
- R.string.action_comment_edit
- )
+ val dateMenuItem = menu.findItem(R.id.menu_date)
+ dateMenuItem.isVisible = dateSettings.endDateSettings != null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
- R.id.menu_comment -> {
+ R.id.menu_date -> {
val supportFragmentManager = activity?.supportFragmentManager ?: return true
- CommentDialogFragment.newInstance(input?.comment)
+ InputDateDialogFragment.newInstance(
+ InputDateSettings(endDateSettings = dateSettings.endDateSettings),
+ input?.startDate ?: Date(),
+ input?.endDate
+ )
.apply {
- setOnCommentDialogFragmentListener(onCommentDialogFragmentListener)
+ setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener)
show(
supportFragmentManager,
- COMMENT_DIALOG_FRAGMENT
+ INPUT_DATE_DIALOG_FRAGMENT
)
}
return true
}
+ R.id.menu_filter -> {
+ val context = context ?: return true
+
+ taxaFilterResultLauncher.launch(
+ TaxaFilterActivity.newIntent(
+ context,
+ filter = getSelectedFilters().toTypedArray()
+ )
+ )
+
+ true
+ }
else -> super.onOptionsItemSelected(item)
}
}
@@ -233,7 +304,15 @@ class InputTaxaSummaryFragment : Fragment(),
}
override fun getSubtitle(): CharSequence? {
- return input?.getCurrentSelectedInputTaxon()?.taxon?.name
+ val context = context ?: return null
+
+ return input?.getInputTaxa()?.size?.let {
+ context.resources.getQuantityString(
+ R.plurals.summary_taxa_subtitle,
+ it,
+ it
+ )
+ }
}
override fun pagingEnabled(): Boolean {
@@ -241,22 +320,127 @@ class InputTaxaSummaryFragment : Fragment(),
}
override fun validate(): Boolean {
- return this.input?.getCurrentSelectedInputTaxon() != null
+ return startEditTaxon || (this.input?.getInputTaxa() ?: emptyList()).all {
+ it is InputTaxon && it.properties.isNotEmpty() && it.getCounting().isNotEmpty()
+ }
}
override fun refreshView() {
// FIXME: this is a workaround to refresh adapter's list as getInputTaxa() items are not immutable...
if ((adapter?.itemCount ?: 0) > 0) adapter?.clear()
- adapter?.setItems(input?.getInputTaxa() ?: emptyList())
+
+ val selectedFilters =
+ savedState.getParcelableArray(KEY_SELECTED_FILTERS)?.map { it as Filter<*> }
+ ?.toList() ?: emptyList()
+ val filterByTaxonomy =
+ selectedFilters.find { filter -> filter.type == Filter.FilterType.TAXONOMY }?.value as Taxonomy?
+
+ adapter?.setItems((input?.getInputTaxa() ?: emptyList()).filter {
+ val taxonomy = filterByTaxonomy ?: return@filter true
+
+ // filter by kingdom only
+ if (taxonomy.group == Taxonomy.ANY) {
+ return@filter it.taxon.taxonomy.kingdom == taxonomy.kingdom
+ }
+
+ it.taxon.taxonomy == taxonomy
+ })
+ }
+
+ private fun applyFilters(vararg filter: Filter<*>) {
+ savedState.putParcelableArray(
+ KEY_SELECTED_FILTERS,
+ filter
+ )
+
+ val selectedTaxonomy =
+ filter.find { it.type == Filter.FilterType.TAXONOMY }?.value as Taxonomy?
+
+ filterByTaxonomy(selectedTaxonomy)
+ refreshView()
+ }
+
+ private fun filterByTaxonomy(selectedTaxonomy: Taxonomy?) {
+ val filterChipGroup = filterChipGroup ?: return
+ val context = context ?: return
+
+ val taxonomyChipsToDelete = arrayListOf()
+
+ for (i in 0 until filterChipGroup.childCount) {
+ with(filterChipGroup[i]) {
+ if (this is Chip && tag is Taxonomy) {
+ taxonomyChipsToDelete.add(this)
+ }
+ }
+ }
+
+ taxonomyChipsToDelete.forEach {
+ filterChipGroup.removeView(it)
+ }
+
+ filterChipGroup.visibility = if (filterChipGroup.childCount > 0) View.VISIBLE else View.GONE
+
+ if (selectedTaxonomy != null) {
+ filterChipGroup.visibility = View.VISIBLE
+
+ // build kingdom taxonomy filter chip
+ with(
+ LayoutInflater.from(context).inflate(
+ R.layout.chip,
+ filterChipGroup,
+ false
+ ) as Chip
+ ) {
+ tag = Taxonomy(selectedTaxonomy.kingdom)
+ text = selectedTaxonomy.kingdom
+ setOnClickListener {
+ applyFilters(*getSelectedFilters().filter { it.type != Filter.FilterType.TAXONOMY }
+ .toTypedArray())
+ }
+ setOnCloseIconClickListener {
+ applyFilters(*getSelectedFilters().filter { it.type != Filter.FilterType.TAXONOMY }
+ .toTypedArray())
+ }
+
+ filterChipGroup.addView(this)
+ }
+
+ // build group taxonomy filter chip
+ if (selectedTaxonomy.group != Taxonomy.ANY) {
+ with(
+ LayoutInflater.from(context).inflate(
+ R.layout.chip,
+ filterChipGroup,
+ false
+ ) as Chip
+ ) {
+ tag = selectedTaxonomy
+ text = selectedTaxonomy.group
+ setOnClickListener {
+ applyFilters(*(getSelectedFilters().filter { filter -> filter.type != Filter.FilterType.TAXONOMY }
+ .toTypedArray() + mutableListOf(FilterTaxonomy(Taxonomy((it.tag as Taxonomy).kingdom)))))
+ }
+ setOnCloseIconClickListener {
+ applyFilters(*(getSelectedFilters().filter { filter -> filter.type != Filter.FilterType.TAXONOMY }
+ .toTypedArray() + mutableListOf(FilterTaxonomy(Taxonomy((it.tag as Taxonomy).kingdom)))))
+ }
+
+ filterChipGroup.addView(this)
+ }
+ }
+ }
}
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
+ private fun getSelectedFilters(): List> {
+ return savedState.getParcelableArray(KEY_SELECTED_FILTERS)?.map { it as Filter<*> }
+ ?.toList() ?: emptyList()
}
companion object {
- private const val COMMENT_DIALOG_FRAGMENT = "comment_dialog_fragment"
+ private const val INPUT_DATE_DIALOG_FRAGMENT = "input_date_dialog_fragment"
+ private const val ARG_DATE_SETTINGS = "arg_date_settings"
+ private const val KEY_SELECTED_FILTERS = "key_selected_filters"
/**
* Use this factory method to create a new instance of [InputTaxaSummaryFragment].
@@ -264,6 +448,13 @@ class InputTaxaSummaryFragment : Fragment(),
* @return A new instance of [InputTaxaSummaryFragment]
*/
@JvmStatic
- fun newInstance() = InputTaxaSummaryFragment()
+ fun newInstance(dateSettings: InputDateSettings) = InputTaxaSummaryFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(
+ ARG_DATE_SETTINGS,
+ dateSettings
+ )
+ }
+ }
}
}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt
index b89db9f5..053cbdf0 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/summary/InputTaxaSummaryRecyclerViewAdapter.kt
@@ -1,13 +1,10 @@
package fr.geonature.occtax.ui.input.summary
import android.text.Spanned
-import android.view.LayoutInflater
+import android.text.SpannedString
import android.view.View
import android.widget.TextView
import androidx.core.text.HtmlCompat
-import com.google.android.material.chip.Chip
-import com.google.android.material.chip.ChipGroup
-import fr.geonature.commons.data.entity.Taxonomy
import fr.geonature.commons.input.AbstractInputTaxon
import fr.geonature.commons.ui.adapter.AbstractListItemRecyclerViewAdapter
import fr.geonature.occtax.R
@@ -58,61 +55,28 @@ class InputTaxaSummaryRecyclerViewAdapter(listener: OnListItemRecyclerViewAdapte
inner class ViewHolder(itemView: View) :
AbstractListItemRecyclerViewAdapter.AbstractViewHolder(itemView) {
private val title: TextView = itemView.findViewById(android.R.id.title)
- private val filterChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group_filter)
private val text1: TextView = itemView.findViewById(android.R.id.text1)
+ private val summary: TextView = itemView.findViewById(android.R.id.summary)
private val text2: TextView = itemView.findViewById(android.R.id.text2)
override fun onBind(item: AbstractInputTaxon) {
title.text = item.taxon.name
- buildTaxonomyChips(item.taxon.taxonomy)
- text1.text = buildInformation(*(item as InputTaxon).properties.values.toTypedArray())
- text1.isSelected = true
+ text1.text = item.taxon.commonName
+ summary.text = buildInformation(*(item as InputTaxon).properties.values.toTypedArray())
+ summary.isSelected = true
text2.text = buildCounting(item.getCounting().size)
}
- private fun buildTaxonomyChips(taxonomy: Taxonomy) {
- filterChipGroup.removeAllViews()
-
- // build kingdom taxonomy filter chip
- with(
- LayoutInflater.from(itemView.context).inflate(
- R.layout.chip,
- filterChipGroup,
- false
- ) as Chip
- ) {
- text = taxonomy.kingdom
- filterChipGroup.addView(this)
- isCloseIconVisible = false
- isEnabled = false
- }
-
- // build group taxonomy filter chip
- if (taxonomy.group != Taxonomy.ANY) {
- with(
- LayoutInflater.from(itemView.context).inflate(
- R.layout.chip,
- filterChipGroup,
- false
- ) as Chip
- ) {
- text = taxonomy.group
- filterChipGroup.addView(this)
- isCloseIconVisible = false
- isEnabled = false
- }
- }
- }
-
private fun buildInformation(vararg propertyValue: PropertyValue): Spanned {
- return HtmlCompat.fromHtml(propertyValue
+ return if (propertyValue.isEmpty()) SpannedString(itemView.context.getString(R.string.summary_taxon_information_empty))
+ else HtmlCompat.fromHtml(propertyValue
.asSequence()
.filterNot { it.isEmpty() }
.map {
itemView.context.getString(
R.string.summary_taxon_information,
getNomenclatureTypeLabel(it.code),
- it.label
+ it.label ?: it.value
)
}
.joinToString(", "),
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt
index 1733b765..94e9913b 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaFragment.kt
@@ -15,6 +15,7 @@ import android.view.animation.AnimationUtils
import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.get
import androidx.fragment.app.Fragment
@@ -33,15 +34,11 @@ import fr.geonature.commons.data.entity.Taxon
import fr.geonature.commons.data.entity.TaxonWithArea
import fr.geonature.commons.data.entity.Taxonomy
import fr.geonature.commons.data.helper.ProviderHelper.buildUri
-import fr.geonature.commons.input.AbstractInput
import fr.geonature.commons.util.ThemeUtils
import fr.geonature.occtax.R
-import fr.geonature.occtax.input.Input
import fr.geonature.occtax.input.InputTaxon
import fr.geonature.occtax.settings.AppSettings
-import fr.geonature.occtax.ui.input.IInputFragment
-import fr.geonature.viewpager.ui.AbstractPagerFragmentActivity
-import fr.geonature.viewpager.ui.IValidateFragment
+import fr.geonature.occtax.ui.input.AbstractInputFragment
import org.tinylog.Logger
import java.util.Locale
import javax.inject.Inject
@@ -52,9 +49,7 @@ import javax.inject.Inject
* @author S. Grimault
*/
@AndroidEntryPoint
-class TaxaFragment : Fragment(),
- IValidateFragment,
- IInputFragment {
+class TaxaFragment : AbstractInputFragment() {
@ContentProviderAuthority
@Inject
@@ -63,7 +58,6 @@ class TaxaFragment : Fragment(),
private lateinit var savedState: Bundle
private lateinit var taxaFilterResultLauncher: ActivityResultLauncher
- private var input: Input? = null
private var adapter: TaxaRecyclerViewAdapter? = null
private var progressBar: ProgressBar? = null
private var emptyTextView: View? = null
@@ -126,7 +120,7 @@ class TaxaFragment : Fragment(),
null,
taxonFilter.first,
taxonFilter.second.map { it.toString() }.toTypedArray(),
- TaxonWithArea.OrderBy().by(AbstractTaxon.COLUMN_NAME).build()
+ TaxonWithArea.OrderBy().byName(args?.getString(KEY_FILTER_BY_NAME)).build()
)
}
LOADER_TAXON -> {
@@ -167,7 +161,8 @@ class TaxaFragment : Fragment(),
when (loader.id) {
LOADER_TAXA -> {
adapter?.bind(data)
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
+ (activity as AppCompatActivity?)?.supportActionBar?.subtitle = getSubtitle()
}
LOADER_TAXON -> {
if (data.moveToFirst()) {
@@ -235,14 +230,14 @@ class TaxaFragment : Fragment(),
Logger.info { "selected taxon (id: ${taxon.id}, name: '${taxon.name}', taxonomy: (kingdom='${taxon.taxonomy.kingdom}', group='${taxon.taxonomy.group}'))" }
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
override fun onNoTaxonSelected() {
input?.getCurrentSelectedInputTaxon()
?.also { input?.removeInputTaxon(it.taxon.id) }
- (activity as AbstractPagerFragmentActivity?)?.validateCurrentPage()
+ listener.validateCurrentPage()
}
override fun scrollToFirstSelectedItemPosition(position: Int) {
@@ -381,13 +376,15 @@ class TaxaFragment : Fragment(),
}
override fun getSubtitle(): CharSequence? {
+ val context = context ?: return null
+
if (progressBar?.visibility == View.VISIBLE && adapter?.itemCount == 0) {
return null
}
val taxaFound = adapter?.itemCount ?: return null
- return resources.getQuantityString(
+ return context.resources.getQuantityString(
R.plurals.taxa_found,
taxaFound,
taxaFound
@@ -403,6 +400,12 @@ class TaxaFragment : Fragment(),
}
override fun refreshView() {
+ if (input?.selectedFeatureId.isNullOrEmpty()) savedState.remove(KEY_SELECTED_FEATURE_ID)
+ else savedState.putString(
+ KEY_SELECTED_FEATURE_ID,
+ input?.selectedFeatureId
+ )
+
loadTaxa()
val selectedInputTaxon = this.input?.getCurrentSelectedInputTaxon()
@@ -424,19 +427,6 @@ class TaxaFragment : Fragment(),
}
}
- override fun setInput(input: AbstractInput) {
- this.input = input as Input
-
- savedState.putString(
- KEY_SELECTED_FEATURE_ID,
- input.selectedFeatureId
- )
-
- if (input.selectedFeatureId.isNullOrEmpty()) {
- savedState.remove(KEY_SELECTED_FEATURE_ID)
- }
- }
-
private fun loadTaxa() {
progressBar?.visibility = View.VISIBLE
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt
index b8445e07..7169590b 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/input/taxa/TaxaRecyclerViewAdapter.kt
@@ -83,8 +83,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis
val checkbox: CheckBox = v.findViewById(android.R.id.checkbox)
checkbox.isChecked = !checkbox.isChecked
- val summary: TextView = v.findViewById(android.R.id.summary)
- summary.isSelected = true
+ v.isSelected = true
val taxon = v.tag as AbstractTaxon
@@ -227,8 +226,7 @@ class TaxaRecyclerViewAdapter(private val listener: OnTaxaRecyclerViewAdapterLis
"${if (taxon.description.isNullOrBlank()) "" else "${taxon.description ?: ""}"}${if (taxon.rank.isNullOrBlank()) "" else "${if (taxon.description.isNullOrBlank()) "" else " - [${taxon.rank}]"} "}",
HtmlCompat.FROM_HTML_MODE_COMPACT
)
- summary.isSelected = selectedTaxon?.id == taxon.id
-
+ itemView.isSelected = selectedTaxon?.id == taxon.id
checkbox.isChecked = selectedTaxon?.id == taxon.id
with(taxon.taxonArea) {
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt
index 62aaa830..0ce1c839 100644
--- a/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/observers/InputObserverListFragment.kt
@@ -41,6 +41,7 @@ class InputObserverListFragment : Fragment() {
@Inject
lateinit var authority: String
+ private lateinit var savedState: Bundle
private var listener: OnInputObserverListFragmentListener? = null
private var adapter: InputObserverRecyclerViewAdapter? = null
@@ -52,7 +53,8 @@ class InputObserverListFragment : Fragment() {
return when (id) {
LOADER_OBSERVERS -> {
- val selections = InputObserver.filter(args?.getString(KEY_FILTER))
+ val observersFilter =
+ InputObserver.Filter().byName(args?.getString(KEY_FILTER)).build()
CursorLoader(
requireContext(),
@@ -61,9 +63,9 @@ class InputObserverListFragment : Fragment() {
InputObserver.TABLE_NAME
),
null,
- selections.first,
- selections.second,
- null
+ observersFilter.first,
+ observersFilter.second.map { it.toString() }.toTypedArray(),
+ InputObserver.OrderBy().byName(args?.getString(KEY_FILTER)).build()
)
}
@@ -96,6 +98,15 @@ class InputObserverListFragment : Fragment() {
mode: ActionMode?,
menu: Menu?
): Boolean {
+ mode?.menuInflater?.inflate(
+ R.menu.search,
+ menu
+ )
+
+ (menu?.findItem(R.id.action_search)?.actionView as SearchView?)?.apply {
+ configureSearchView(this)
+ }
+
return true
}
@@ -103,7 +114,17 @@ class InputObserverListFragment : Fragment() {
mode: ActionMode?,
menu: Menu?
): Boolean {
- return false
+ val searchCriterion = savedState.getString(KEY_FILTER)
+
+ (menu?.findItem(R.id.action_search)?.actionView as SearchView?)?.apply {
+ isIconified = searchCriterion.isNullOrEmpty()
+ setQuery(
+ searchCriterion,
+ false
+ )
+ }
+
+ return !searchCriterion.isNullOrEmpty()
}
override fun onActionItemClicked(
@@ -119,6 +140,12 @@ class InputObserverListFragment : Fragment() {
}
}
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ savedState = savedInstanceState ?: Bundle()
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -192,6 +219,10 @@ class InputObserverListFragment : Fragment() {
setHasOptionsMenu(true)
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(savedState.apply { putAll(outState) })
+ }
+
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater
@@ -207,29 +238,9 @@ class InputObserverListFragment : Fragment() {
menu
)
- val searchItem = menu.findItem(R.id.action_search)
- val searchView = searchItem.actionView as SearchView
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- return true
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- LoaderManager.getInstance(this@InputObserverListFragment)
- .restartLoader(
- LOADER_OBSERVERS,
- bundleOf(
- Pair(
- KEY_FILTER,
- newText
- )
- ),
- loaderCallbacks
- )
-
- return true
- }
- })
+ (menu.findItem(R.id.action_search)?.actionView as SearchView?)?.apply {
+ configureSearchView(this)
+ }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -259,6 +270,35 @@ class InputObserverListFragment : Fragment() {
listener = null
}
+ private fun configureSearchView(searchView: SearchView) {
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String): Boolean {
+ savedState.putString(
+ KEY_FILTER,
+ newText
+ )
+
+ LoaderManager.getInstance(this@InputObserverListFragment)
+ .restartLoader(
+ LOADER_OBSERVERS,
+ bundleOf(
+ Pair(
+ KEY_FILTER,
+ newText
+ )
+ ),
+ loaderCallbacks
+ )
+
+ return true
+ }
+ })
+ }
+
private fun updateActionMode(inputObservers: List) {
if (inputObservers.isEmpty()) {
actionMode?.finish()
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt
deleted file mode 100644
index e18030b5..00000000
--- a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/CommentDialogFragment.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package fr.geonature.occtax.ui.shared.dialog
-
-import android.app.Dialog
-import android.os.Bundle
-import android.text.Editable
-import android.text.TextUtils
-import android.text.TextWatcher
-import android.view.View
-import android.view.ViewGroup
-import android.widget.EditText
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import fr.geonature.commons.util.KeyboardUtils.hideSoftKeyboard
-import fr.geonature.commons.util.KeyboardUtils.showSoftKeyboard
-import fr.geonature.occtax.R
-
-/**
- * Custom [Dialog] used to add comment.
- *
- * @author S. Grimault
- */
-class CommentDialogFragment : DialogFragment() {
-
- private var comment: String? = null
- private var onCommentDialogFragmentListener: OnCommentDialogFragmentListener? = null
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val context = requireContext()
-
- val view = View.inflate(
- context,
- R.layout.dialog_comment,
- null
- )
- val editText = view.findViewById(android.R.id.edit)
- .also {
- it.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(
- s: CharSequence?,
- start: Int,
- count: Int,
- after: Int
- ) {
- }
-
- override fun afterTextChanged(s: Editable?) {
- }
-
- override fun onTextChanged(
- s: CharSequence?,
- start: Int,
- before: Int,
- count: Int
- ) {
- comment = s?.toString()
- }
- })
- }
-
- arguments?.getString(KEY_COMMENT)
- ?.also {
- comment = it
- editText.text = Editable.Factory.getInstance()
- .newEditable(it)
- }
-
- // restore the previous state if any
- savedInstanceState?.getString(KEY_COMMENT)
- ?.also {
- comment = it
- editText.text = Editable.Factory.getInstance()
- .newEditable(it)
- }
-
- // show automatically the soft keyboard for the EditText
- editText.post {
- showSoftKeyboard(editText)
- }
-
- return AlertDialog.Builder(context)
- .setTitle(if (TextUtils.isEmpty(comment)) R.string.alert_dialog_add_comment_title else R.string.alert_dialog_edit_comment_title)
- .setView(view)
- .setPositiveButton(R.string.alert_dialog_ok) { _, _ ->
- hideSoftKeyboard(editText)
- onCommentDialogFragmentListener?.onChanged(comment)
- }
- .setNegativeButton(
- R.string.alert_dialog_cancel,
- null
- )
- .create()
- }
-
- override fun onStart() {
- super.onStart()
-
- // resize the dialog width to match parent
- dialog?.also {
- it.window?.setLayout(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- outState.putSerializable(
- KEY_COMMENT,
- comment
- )
-
- super.onSaveInstanceState(outState)
- }
-
- fun setOnCommentDialogFragmentListener(onCommentDialogFragmentListener: OnCommentDialogFragmentListener) {
-
- this.onCommentDialogFragmentListener = onCommentDialogFragmentListener
- }
-
- companion object {
-
- const val KEY_COMMENT = "comment"
-
- /**
- * Use this factory method to create a new instance of [CommentDialogFragment].
- *
- * @return A new instance of [CommentDialogFragment]
- */
- @JvmStatic
- fun newInstance(comment: String?) = CommentDialogFragment().apply {
- arguments = Bundle().apply {
- putString(
- KEY_COMMENT,
- comment
- )
- }
- }
- }
-
- /**
- * The callback used by [CommentDialogFragment].
- *
- * @author S. Grimault
- */
- interface OnCommentDialogFragmentListener {
-
- /**
- * Invoked when the positive button of the dialog is pressed.
- *
- * @param comment the string comment edited from this dialog
- */
- fun onChanged(comment: String?)
- }
-}
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt
new file mode 100644
index 00000000..2c4c686e
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/dialog/InputDateDialogFragment.kt
@@ -0,0 +1,169 @@
+package fr.geonature.occtax.ui.shared.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import fr.geonature.occtax.R
+import fr.geonature.occtax.settings.InputDateSettings
+import fr.geonature.occtax.ui.shared.view.InputDateView
+import java.util.Date
+
+/**
+ * Custom [Dialog] used to edit input date.
+ *
+ * @author S. Grimault
+ */
+class InputDateDialogFragment : DialogFragment() {
+
+ private var onInputDateDialogFragmentListener: OnInputDateDialogFragmentListener? = null
+
+ private var dateSettings: InputDateSettings =
+ InputDateSettings(endDateSettings = InputDateSettings.DateSettings.DATE)
+ private var startDate: Date = Date()
+ private var endDate: Date = startDate
+ private var buttonValidate: Button? = null
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val context = requireContext()
+
+ val view = View.inflate(
+ context,
+ R.layout.dialog_date,
+ null
+ )
+
+ // restore the previous state if any
+ dateSettings = (savedInstanceState?.getParcelable(KEY_DATE_SETTINGS)
+ ?: arguments?.getParcelable(KEY_DATE_SETTINGS)
+ ?: InputDateSettings(endDateSettings = InputDateSettings.DateSettings.DATE))
+ startDate = (savedInstanceState?.getSerializable(KEY_DATE_START) as Date?)
+ ?: (arguments?.getSerializable(KEY_DATE_START) as Date?) ?: Date()
+ endDate = (savedInstanceState?.getSerializable(KEY_DATE_END) as Date?)
+ ?: (arguments?.getSerializable(KEY_DATE_END) as Date?) ?: startDate
+
+ view.findViewById(R.id.input_date)?.also {
+ it.setInputDateSettings(dateSettings)
+ it.setDates(
+ startDate,
+ endDate
+ )
+ it.setListener(object : InputDateView.OnInputDateViewListener {
+ override fun fragmentManager(): FragmentManager? {
+ return activity?.supportFragmentManager
+ }
+
+ override fun onDatesChanged(startDate: Date, endDate: Date) {
+ this@InputDateDialogFragment.startDate = startDate
+ this@InputDateDialogFragment.endDate = endDate
+
+ buttonValidate?.isEnabled = true
+ }
+
+ override fun hasError(message: CharSequence) {
+ // disable validate button unless start and end date are valid
+ buttonValidate?.isEnabled = false
+ }
+ })
+ }
+
+ val alertDialog = AlertDialog.Builder(context)
+ .setTitle(
+ if (dateSettings.startDateSettings != null && dateSettings.endDateSettings == null) R.string.input_date_start_hint
+ else if (dateSettings.startDateSettings == null && dateSettings.endDateSettings != null) R.string.input_date_end_hint
+ else R.string.input_date_hint
+ )
+ .setView(view)
+ .setPositiveButton(R.string.alert_dialog_ok) { _, _ ->
+ onInputDateDialogFragmentListener?.onDatesChanged(
+ startDate,
+ endDate
+ )
+ }
+ .setNegativeButton(
+ R.string.alert_dialog_cancel,
+ null
+ )
+ .create()
+
+ alertDialog.setOnShowListener {
+ buttonValidate = (it as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
+ }
+
+ return alertDialog
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ with(outState) {
+ putParcelable(
+ KEY_DATE_SETTINGS,
+ dateSettings
+ )
+ putSerializable(
+ KEY_DATE_START,
+ startDate
+ )
+ putSerializable(
+ KEY_DATE_END,
+ endDate
+ )
+ }
+
+ super.onSaveInstanceState(outState)
+ }
+
+ fun setOnInputDateDialogFragmentListenerListener(onInputDateDialogFragmentListener: OnInputDateDialogFragmentListener) {
+ this.onInputDateDialogFragmentListener = onInputDateDialogFragmentListener
+ }
+
+ companion object {
+
+ const val KEY_DATE_SETTINGS = "key_settings"
+ const val KEY_DATE_START = "key_date_start"
+ const val KEY_DATE_END = "key_date_end"
+
+ /**
+ * Use this factory method to create a new instance of [InputDateDialogFragment].
+ *
+ * @return A new instance of [InputDateDialogFragment]
+ */
+ @JvmStatic
+ fun newInstance(dateSettings: InputDateSettings, startDate: Date, endDate: Date?) =
+ InputDateDialogFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(
+ KEY_DATE_SETTINGS,
+ dateSettings
+ )
+ putSerializable(
+ KEY_DATE_START,
+ startDate
+ )
+ putSerializable(
+ KEY_DATE_END,
+ endDate ?: startDate
+ )
+ }
+ }
+ }
+
+ /**
+ * The callback used by [InputDateDialogFragment].
+ *
+ * @author S. Grimault
+ */
+ interface OnInputDateDialogFragmentListener {
+
+ /**
+ * Invoked when the positive button of the dialog is pressed.
+ *
+ * @param startDate the start date edited from this dialog
+ * @param endDate the end date edited from this dialog
+ */
+ fun onDatesChanged(startDate: Date, endDate: Date)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt
new file mode 100644
index 00000000..a2ae664b
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/ActionView.kt
@@ -0,0 +1,225 @@
+package fr.geonature.occtax.ui.shared.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.children
+import fr.geonature.occtax.R
+
+/**
+ * Generic [View] about adding custom view with an action button.
+ *
+ * @author S. Grimault
+ */
+class ActionView : ConstraintLayout {
+
+ private var contentView: FrameLayout? = null
+ private lateinit var titleTextView: TextView
+ private lateinit var actionButton: Button
+ private lateinit var emptyTextView: TextView
+ private var listener: OnActionViewListener? = null
+
+ private var contentViewVisibility: Int = View.GONE
+
+ @StringRes
+ private var actionText: Int = 0
+
+ @StringRes
+ private var actionEmptyText: Int = 0
+
+ constructor(context: Context) : super(context) {
+ init(
+ null,
+ 0
+ )
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet
+ ) : super(
+ context,
+ attrs
+ ) {
+ init(
+ attrs,
+ 0
+ )
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(
+ context,
+ attrs,
+ defStyleAttr
+ ) {
+ init(
+ attrs,
+ defStyleAttr
+ )
+ }
+
+ override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+ if (contentView == null) {
+ super.addView(
+ child,
+ index,
+ params
+ )
+ } else {
+ contentView?.children?.asSequence()?.filter { it.id != emptyTextView.id }?.forEach {
+ contentView?.removeView(it)
+ }
+ contentView?.addView(
+ child,
+ index,
+ params
+ )
+ setContentViewVisibility(contentViewVisibility)
+ }
+ }
+
+ fun getContentView(): View? {
+ return contentView?.children?.asSequence()?.firstOrNull { it.id != emptyTextView.id }
+ }
+
+ fun setListener(listener: OnActionViewListener) {
+ this.listener = listener
+ }
+
+ fun setTitle(@StringRes titleResourceId: Int) {
+ setTitle(if (titleResourceId == 0) null else context.getString(titleResourceId))
+ }
+
+ fun setTitle(title: String?) {
+ titleTextView.text = title
+ titleTextView.visibility = if (title.isNullOrBlank()) GONE else VISIBLE
+ }
+
+ fun setEmptyText(@StringRes emptyTextResourceId: Int) {
+ emptyTextView.setText(if (emptyTextResourceId == 0) R.string.no_data else emptyTextResourceId)
+ }
+
+ fun enableActionButton(enabled: Boolean = true) {
+ actionButton.isEnabled = enabled
+ }
+
+ fun setActionText(@StringRes actionResourceId: Int) {
+ if (actionResourceId == 0) {
+ return
+ }
+
+ actionText = actionResourceId
+ actionEmptyText = actionResourceId
+ }
+
+ fun setActionEmptyText(@StringRes actionResourceId: Int) {
+ if (actionResourceId == 0) {
+ return
+ }
+
+ actionEmptyText = actionResourceId
+ }
+
+ fun setContentViewVisibility(visibility: Int) {
+ contentViewVisibility = visibility
+ actionButton.setText(if (visibility == View.VISIBLE) actionText else actionEmptyText.takeIf { it > 0 }
+ ?: actionText)
+ contentView?.children?.asSequence()?.forEach {
+ if (it.id == emptyTextView.id) it.visibility =
+ if (visibility == View.VISIBLE) View.GONE else View.VISIBLE
+ else it.visibility = if (visibility == View.VISIBLE) View.VISIBLE else View.GONE
+ }
+ }
+
+ private fun init(
+ attrs: AttributeSet?,
+ defStyle: Int
+ ) {
+ View.inflate(
+ context,
+ R.layout.view_action,
+ this
+ )
+
+ titleTextView = findViewById(android.R.id.title)
+ actionButton = findViewById(android.R.id.button1)
+ actionButton.setOnClickListener { listener?.onAction() }
+ contentView = findViewById(android.R.id.content)
+ emptyTextView = findViewById(android.R.id.empty)
+
+ // load attributes
+ val ta = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.ActionView,
+ defStyle,
+ 0
+ )
+
+ ta.getString(R.styleable.ActionView_title)?.also {
+ setTitle(it)
+ }
+ setTitle(
+ ta.getResourceId(
+ R.styleable.ActionView_title,
+ 0
+ )
+ )
+
+ setEmptyText(
+ ta.getResourceId(
+ R.styleable.ActionView_no_data,
+ R.string.no_data
+ )
+ )
+
+ enableActionButton(
+ ta.getBoolean(
+ R.styleable.ActionView_action_enabled,
+ true
+ )
+ )
+
+ setActionText(
+ ta.getResourceId(
+ R.styleable.ActionView_action,
+ 0
+ )
+ )
+ setActionEmptyText(
+ ta.getResourceId(
+ R.styleable.ActionView_action_empty,
+ 0
+ )
+ )
+
+ setContentViewVisibility(
+ ta.getInt(
+ R.styleable.ActionView_content_visibility,
+ View.GONE
+ )
+ )
+
+ ta.recycle()
+ }
+
+ /**
+ * Callback used by [ActionView].
+ */
+ interface OnActionViewListener {
+
+ /**
+ * Called when the action button has been clicked.
+ */
+ fun onAction()
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt
new file mode 100644
index 00000000..aa873084
--- /dev/null
+++ b/occtax/src/main/java/fr/geonature/occtax/ui/shared/view/InputDateView.kt
@@ -0,0 +1,447 @@
+package fr.geonature.occtax.ui.shared.view
+
+import android.content.Context
+import android.text.Editable
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.view.View
+import android.widget.EditText
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.fragment.app.FragmentManager
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.DateValidatorPointBackward
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.textfield.TextInputLayout
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import fr.geonature.commons.input.AbstractInput
+import fr.geonature.commons.util.afterTextChanged
+import fr.geonature.commons.util.get
+import fr.geonature.commons.util.set
+import fr.geonature.occtax.R
+import fr.geonature.occtax.settings.InputDateSettings
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.Calendar
+import java.util.Date
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * Generic [View] about [AbstractInput] start and end date.
+ *
+ * @author S. Grimault
+ */
+class InputDateView : ConstraintLayout {
+
+ private lateinit var titleTextView: TextView
+ private lateinit var dateStartTextInputLayout: TextInputLayout
+ private lateinit var dateEndTextInputLayout: TextInputLayout
+
+ private var dateSettings: InputDateSettings = InputDateSettings.DEFAULT
+ private var startDate: Date = Date()
+ private var endDate: Date = startDate
+
+ private var listener: OnInputDateViewListener? = null
+
+ constructor(context: Context) : super(context) {
+ init(
+ null,
+ 0
+ )
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(
+ context,
+ attrs
+ ) {
+ init(
+ attrs,
+ 0
+ )
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
+ context,
+ attrs,
+ defStyle
+ ) {
+ init(
+ attrs,
+ defStyle
+ )
+ }
+
+ fun setListener(listener: OnInputDateViewListener) {
+ this.listener = listener
+ }
+
+ fun setTitle(@StringRes titleResourceId: Int) {
+ setTitle(if (titleResourceId == 0) null else context.getString(titleResourceId))
+ }
+
+ fun setTitle(title: String?) {
+ titleTextView.text = title
+ titleTextView.visibility = if (title.isNullOrBlank()) GONE else VISIBLE
+ }
+
+ fun setInputDateSettings(dateSettings: InputDateSettings) {
+ this.dateSettings = dateSettings
+
+ with(dateStartTextInputLayout) {
+ visibility = if (dateSettings.startDateSettings == null) View.GONE else View.VISIBLE
+ hint = context.getString(
+ if (dateSettings.endDateSettings == null) R.string.input_date_hint
+ else R.string.input_date_start_hint
+ )
+ }
+ dateEndTextInputLayout.visibility =
+ if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE
+ }
+
+ fun setDates(startDate: Date, endDate: Date?) {
+ this.startDate = startDate
+ this.endDate = endDate ?: startDate
+
+ dateStartTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
+ startDate
+ )
+ }
+ dateEndTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
+ endDate
+ )
+ }
+ }
+
+ fun hasErrors(): Boolean {
+ return checkStartDateConstraints() != null ||
+ checkEndDateConstraints() != null
+ }
+
+ private fun init(attrs: AttributeSet?, defStyle: Int) {
+ View.inflate(
+ context,
+ R.layout.view_input_date,
+ this
+ )
+
+ titleTextView = findViewById(android.R.id.title)
+
+ dateStartTextInputLayout = findViewById(R.id.dateStart).apply {
+ visibility = if (dateSettings.startDateSettings == null) View.GONE else View.VISIBLE
+ hint = context.getString(
+ if (dateSettings.endDateSettings == null) R.string.input_date_hint
+ else R.string.input_date_start_hint
+ )
+ editText?.afterTextChanged {
+ error = checkStartDateConstraints()
+ dateEndTextInputLayout.error = checkEndDateConstraints()
+
+ error?.also {
+ listener?.hasError(it)
+ }
+ }
+ editText?.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ val startDate = selectDateTime(
+ CalendarConstraints
+ .Builder()
+ .setValidator(DateValidatorPointBackward.now())
+ .build(),
+ dateSettings.startDateSettings == InputDateSettings.DateSettings.DATETIME,
+ startDate
+ )
+
+ this@InputDateView.startDate = startDate
+
+ if (dateSettings.endDateSettings == null) {
+ this@InputDateView.endDate = startDate
+ }
+
+ dateStartTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
+ startDate
+ )
+ }
+ dateEndTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
+ endDate
+ )
+ }
+
+ if (error == null && dateEndTextInputLayout.editText?.error == null) {
+ listener?.onDatesChanged(
+ startDate,
+ endDate
+ )
+ }
+ }
+ }
+ }
+
+ dateEndTextInputLayout = findViewById(R.id.dateEnd).apply {
+ visibility = if (dateSettings.endDateSettings == null) View.GONE else View.VISIBLE
+ editText?.afterTextChanged {
+ error = checkEndDateConstraints()
+ dateStartTextInputLayout.error = checkStartDateConstraints()
+
+ error?.also {
+ listener?.hasError(it)
+ }
+ }
+ editText?.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ val endDate = selectDateTime(
+ CalendarConstraints
+ .Builder()
+ .setValidator(
+ DateValidatorPointForward.from(
+ startDate
+ .set(
+ Calendar.HOUR_OF_DAY,
+ 0
+ ).set(
+ Calendar.MINUTE,
+ 0
+ ).set(
+ Calendar.SECOND,
+ 0
+ ).set(
+ Calendar.MILLISECOND,
+ 0
+ ).time
+ )
+ )
+ .build(),
+ dateSettings.endDateSettings == InputDateSettings.DateSettings.DATETIME,
+ endDate
+ )
+
+ this@InputDateView.endDate = endDate
+ dateStartTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.startDateSettings ?: InputDateSettings.DateSettings.DATE,
+ startDate
+ )
+ }
+ dateEndTextInputLayout.editText?.apply {
+ updateDateEditText(
+ this,
+ dateSettings.endDateSettings ?: InputDateSettings.DateSettings.DATE,
+ endDate
+ )
+ }
+
+ if (error == null && dateStartTextInputLayout.editText?.error == null) {
+ listener?.onDatesChanged(
+ startDate,
+ endDate
+ )
+ }
+ }
+ }
+ }
+
+ // Load attributes
+ val ta = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.InputDateView,
+ defStyle,
+ 0
+ )
+
+ ta.getString(R.styleable.InputDateView_title)?.also {
+ setTitle(it)
+ }
+ setTitle(
+ ta.getResourceId(
+ R.styleable.InputDateView_title,
+ 0
+ )
+ )
+
+ ta.recycle()
+ }
+
+ /**
+ * Select a new date from given optional date through date/time pickers.
+ * If no date was given, use the current date.
+ */
+ private suspend fun selectDateTime(
+ bounds: CalendarConstraints,
+ withTime: Boolean = false,
+ from: Date = Date()
+ ): Date =
+ suspendCoroutine { continuation ->
+ val fragmentManager = listener?.fragmentManager()
+
+ if (fragmentManager == null) {
+ continuation.resume(from)
+
+ return@suspendCoroutine
+ }
+
+ val context = context
+
+ if (context == null) {
+ continuation.resume(from)
+
+ return@suspendCoroutine
+ }
+
+ with(
+ MaterialDatePicker.Builder
+ .datePicker()
+ .setSelection(from.time)
+ .setCalendarConstraints(bounds)
+ .build()
+ ) {
+ addOnPositiveButtonClickListener {
+ val selectedDate = Date(it).set(
+ Calendar.HOUR_OF_DAY,
+ from.get(Calendar.HOUR_OF_DAY)
+ ).set(
+ Calendar.MINUTE,
+ from.get(Calendar.MINUTE)
+ )
+
+ if (!withTime) {
+ continuation.resume(selectedDate)
+
+ return@addOnPositiveButtonClickListener
+ }
+
+ with(
+ MaterialTimePicker.Builder()
+ .setTimeFormat(if (DateFormat.is24HourFormat(context)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H)
+ .setHour(selectedDate.get(if (DateFormat.is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR))
+ .setMinute(selectedDate.get(Calendar.MINUTE))
+ .build()
+ ) {
+ addOnPositiveButtonClickListener {
+ continuation.resume(
+ selectedDate.set(
+ if (DateFormat.is24HourFormat(context)) Calendar.HOUR_OF_DAY else Calendar.HOUR,
+ hour
+ ).set(
+ Calendar.MINUTE,
+ minute
+ )
+ )
+ }
+ addOnNegativeButtonClickListener {
+ continuation.resume(selectedDate)
+ }
+ addOnCancelListener {
+ continuation.resume(selectedDate)
+ }
+ show(
+ fragmentManager,
+ TIME_PICKER_DIALOG_FRAGMENT
+ )
+ }
+ }
+ addOnNegativeButtonClickListener {
+ continuation.resume(from)
+ }
+ addOnCancelListener {
+ continuation.resume(from)
+ }
+ show(
+ fragmentManager,
+ DATE_PICKER_DIALOG_FRAGMENT
+ )
+ }
+ }
+
+ private fun updateDateEditText(
+ editText: EditText,
+ dateSettings: InputDateSettings.DateSettings,
+ date: Date?
+ ) {
+ editText.text = date?.let {
+ Editable.Factory
+ .getInstance()
+ .newEditable(
+ DateFormat.format(
+ context.getString(
+ if (dateSettings == InputDateSettings.DateSettings.DATETIME) R.string.input_datetime_format
+ else R.string.input_date_format
+ ),
+ it
+ ).toString()
+ )
+ }
+ }
+
+ /**
+ * Checks start date constraints from current [AbstractInput].
+ *
+ * @return `null` if all constraints are valid, or an error message
+ */
+ private fun checkStartDateConstraints(): CharSequence? {
+ if (startDate.after(Date())) {
+ return context.getString(R.string.input_error_date_start_after_now)
+ }
+
+ return null
+ }
+
+ /**
+ * Checks end date constraints from current [AbstractInput].
+ *
+ * @return `null` if all constraints are valid, or an error message
+ */
+ private fun checkEndDateConstraints(): CharSequence? {
+ if (dateSettings.endDateSettings == null) {
+ return null
+ }
+
+ if (startDate.after(endDate)) {
+ return context.getString(R.string.input_error_date_end_before_start_date)
+ }
+
+ return null
+ }
+
+ /**
+ * Callback used by [InputDateView].
+ */
+ interface OnInputDateViewListener {
+
+ /**
+ * Return the FragmentManager for interacting with fragments associated with this view.
+ */
+ fun fragmentManager(): FragmentManager?
+
+ /**
+ * Called when the start and end dates have been changed.
+ */
+ fun onDatesChanged(startDate: Date, endDate: Date)
+
+ /**
+ * Called when the current start or end dates is not valid.
+ */
+ fun hasError(message: CharSequence)
+ }
+
+ companion object {
+ private const val DATE_PICKER_DIALOG_FRAGMENT = "date_picker_dialog_fragment"
+ private const val TIME_PICKER_DIALOG_FRAGMENT = "time_picker_dialog_fragment"
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/main/res/drawable/ic_action_comment.xml b/occtax/src/main/res/drawable/ic_action_comment.xml
deleted file mode 100644
index 4aa3ca17..00000000
--- a/occtax/src/main/res/drawable/ic_action_comment.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/occtax/src/main/res/drawable/ic_action_edit_calendar.xml b/occtax/src/main/res/drawable/ic_action_edit_calendar.xml
index 0a97af78..9f50ba54 100644
--- a/occtax/src/main/res/drawable/ic_action_edit_calendar.xml
+++ b/occtax/src/main/res/drawable/ic_action_edit_calendar.xml
@@ -2,6 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="@color/actionbar_icon_tint"
android:viewportWidth="24"
android:viewportHeight="24">
+
+
diff --git a/occtax/src/main/res/drawable/ic_lock.xml b/occtax/src/main/res/drawable/ic_lock.xml
new file mode 100644
index 00000000..36cc0ed3
--- /dev/null
+++ b/occtax/src/main/res/drawable/ic_lock.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/occtax/src/main/res/drawable/ic_lock_open.xml b/occtax/src/main/res/drawable/ic_lock_open.xml
new file mode 100644
index 00000000..89c86aca
--- /dev/null
+++ b/occtax/src/main/res/drawable/ic_lock_open.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/occtax/src/main/res/layout/dialog_comment.xml b/occtax/src/main/res/layout/dialog_comment.xml
deleted file mode 100644
index dba0daf2..00000000
--- a/occtax/src/main/res/layout/dialog_comment.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/occtax/src/main/res/layout/dialog_date.xml b/occtax/src/main/res/layout/dialog_date.xml
new file mode 100644
index 00000000..95f34940
--- /dev/null
+++ b/occtax/src/main/res/layout/dialog_date.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/occtax/src/main/res/layout/fragment_input_summary.xml b/occtax/src/main/res/layout/fragment_input_summary.xml
new file mode 100644
index 00000000..8806b88b
--- /dev/null
+++ b/occtax/src/main/res/layout/fragment_input_summary.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml
index 98ae1ca8..2dae7249 100644
--- a/occtax/src/main/res/layout/fragment_observers_and_date_input.xml
+++ b/occtax/src/main/res/layout/fragment_observers_and_date_input.xml
@@ -42,95 +42,71 @@
app:cardElevation="@dimen/cardview_elevation"
app:contentPadding="@dimen/padding_default">
-
-
-
-
-
+ app:content_visibility="gone"
+ app:title="@string/observers_and_date_dataset">
-
-
-
-
-
+ android:background="?attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/padding_default"
+ android:paddingTop="@dimen/padding_default">
-
+ android:textAppearance="?attr/textAppearanceListItem"
+ android:textStyle="bold"
+ tools:text="@tools:sample/last_names" />
-
-
-
-
-
+ android:duplicateParentState="true"
+ android:ellipsize="marquee"
+ android:lines="3"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ tools:text="@tools:sample/first_names" />
-
+
+
+
+
+
-
+
+
+
@@ -153,7 +129,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/padding_default"
- android:text="@string/observers_and_date_comment"
+ android:text="@string/input_comment"
android:textAllCaps="false"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textStyle="bold" />
@@ -163,13 +139,12 @@
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:padding="@dimen/padding_default"
app:endIconMode="clear_text">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/occtax/src/main/res/layout/list_selectable_item_3.xml b/occtax/src/main/res/layout/list_item_dataset.xml
similarity index 84%
rename from occtax/src/main/res/layout/list_selectable_item_3.xml
rename to occtax/src/main/res/layout/list_item_dataset.xml
index aaa44a44..21d03682 100644
--- a/occtax/src/main/res/layout/list_selectable_item_3.xml
+++ b/occtax/src/main/res/layout/list_item_dataset.xml
@@ -10,19 +10,11 @@
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-
@@ -43,8 +34,8 @@
android:layout_marginStart="?attr/listPreferredItemPaddingStart"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
+ android:lines="3"
android:scrollHorizontally="true"
- android:singleLine="true"
android:textAppearance="?attr/textAppearanceListItemSecondary"
app:layout_constraintEnd_toEndOf="@android:id/title"
app:layout_constraintStart_toStartOf="@android:id/title"
diff --git a/occtax/src/main/res/layout/list_item_taxon.xml b/occtax/src/main/res/layout/list_item_taxon.xml
index 553e58e7..a8611c0e 100644
--- a/occtax/src/main/res/layout/list_item_taxon.xml
+++ b/occtax/src/main/res/layout/list_item_taxon.xml
@@ -39,10 +39,13 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="?attr/listPreferredItemHeight"
- android:layout_marginEnd="?attr/listPreferredItemPaddingEnd"
android:layout_marginTop="?attr/listPreferredItemPaddingStart"
- android:ellipsize="end"
- android:maxLines="1"
+ android:layout_marginEnd="?attr/listPreferredItemPaddingEnd"
+ android:duplicateParentState="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
android:textAppearance="?attr/textAppearanceListItem"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/taxon_observers_image_view"
@@ -56,12 +59,15 @@
android:layout_height="wrap_content"
android:layout_marginStart="?attr/listPreferredItemHeight"
android:layout_marginEnd="?attr/listPreferredItemPaddingEnd"
- android:ellipsize="end"
- android:maxLines="1"
+ android:duplicateParentState="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
android:textAppearance="?attr/textAppearanceListItemSecondary"
- app:layout_constraintTop_toBottomOf="@android:id/text1"
app:layout_constraintEnd_toStartOf="@+id/taxon_observers_image_view"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@android:id/text1"
tools:text="@tools:sample/first_names" />
+ android:paddingHorizontal="@dimen/padding_default">
-
+ tools:text="@tools:sample/first_names" />
\ No newline at end of file
diff --git a/occtax/src/main/res/layout/view_action.xml b/occtax/src/main/res/layout/view_action.xml
new file mode 100644
index 00000000..2502f501
--- /dev/null
+++ b/occtax/src/main/res/layout/view_action.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/occtax/src/main/res/layout/view_action_more.xml b/occtax/src/main/res/layout/view_action_more.xml
index 355aba48..ec493643 100644
--- a/occtax/src/main/res/layout/view_action_more.xml
+++ b/occtax/src/main/res/layout/view_action_more.xml
@@ -11,7 +11,7 @@
android:id="@android:id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/information_action_more"
+ android:text="@string/nomenclature_more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
diff --git a/occtax/src/main/res/layout/view_input_date.xml b/occtax/src/main/res/layout/view_input_date.xml
new file mode 100644
index 00000000..86cb651a
--- /dev/null
+++ b/occtax/src/main/res/layout/view_input_date.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/occtax/src/main/res/menu/comment.xml b/occtax/src/main/res/menu/date.xml
similarity index 60%
rename from occtax/src/main/res/menu/comment.xml
rename to occtax/src/main/res/menu/date.xml
index fe1cc23a..0a8d42c3 100644
--- a/occtax/src/main/res/menu/comment.xml
+++ b/occtax/src/main/res/menu/date.xml
@@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
\ No newline at end of file
diff --git a/occtax/src/main/res/values-fr/nomenclatures.xml b/occtax/src/main/res/values-fr/nomenclatures.xml
index 296d0d4a..302fafed 100644
--- a/occtax/src/main/res/values-fr/nomenclatures.xml
+++ b/occtax/src/main/res/values-fr/nomenclatures.xml
@@ -10,7 +10,7 @@
Comportement
Niveau de naturalité
Preuve d\'existence
- Commentaire
+ Commentaire du taxon
Stade de vie
Sexe
Objet du dénombrement
diff --git a/occtax/src/main/res/values-fr/strings.xml b/occtax/src/main/res/values-fr/strings.xml
index 73d48c1c..295654d7 100644
--- a/occtax/src/main/res/values-fr/strings.xml
+++ b/occtax/src/main/res/values-fr/strings.xml
@@ -3,6 +3,7 @@
Authentification
Paramètres
+ Editer le relevé
Jeu de données
Observateurs
Filtres sur les taxons
@@ -54,8 +55,6 @@
- %d sélectionnés
Sauvegarder
- Ajouter un commentaire
- Supprimer un commentaire
Nouvelle version disponible
Une nouvelle version est disponible.\nVoulez vous la mettre à jour maintenant ?
@@ -68,9 +67,6 @@
Souhaitez vous supprimer ce relevé ?
Souhaitez vous supprimer ce dénombrement ?
Souhaitez vous supprimer ce taxon ?
- Ajouter un commentaire
- Éditer le commentaire
- Ajouter un commentaire
OK
Annuler
@@ -93,7 +89,8 @@
Observateur & date
Pointage
- Taxons
+ Taxons ajoutés
+ Choix du taxon
Informations
Dénombrements
Bilan de la saisie
@@ -106,19 +103,20 @@
Aucun observateur sélectionné.\nAjouter en un via le bouton "Ajouter".
Jeu de données
- Date
- Date du relevé
- Date de début
- Date de fin
- La date de début n\'a pas été définie
- La date de début ne doit pas se situer dans le futur
- La date de fin n\'a pas été définie
- La date de fin doit être après celle du début
- EEE dd MMM yyyy
- EEE dd MMM yyyy \'à\' HH:mm
- Commentaire du relevé
- Ajouter un commentaire
- Éditer le commentaire
+
+ Date du relevé
+ Date du relevé
+ Date de début
+ Date de fin
+ La date de début n\'a pas été définie
+ La date de début ne doit pas se situer dans le futur
+ La date de fin n\'a pas été définie
+ La date de fin doit être après celle du début
+ EEE dd MMM yyyy
+ EEE dd MMM yyyy \'à\' HH:mm
+ Commentaire du relevé
+ Ajouter un commentaire
+ Éditer le commentaire
- %d taxon trouvé
@@ -153,9 +151,7 @@
- de %d jours
- Informations avancées
- Déterminateur
- Ajouter un commentaire optionnel
+ ⚠ Aucune nomenclature trouvée.\nUne synchronisation des données est nécessaire avant tout relevé.
Aucun dénombrement effectué.\nCréer un nouveau dénombrement via le bouton +
Dénombrement #%d
@@ -166,14 +162,20 @@
Min
Max
+
+ - %d taxon
+ - %d taxons
+ - %d taxons
+
Aucune saisie.\nAjouter un nouveau taxon via le bouton +.
%1$s :]]> %2$s]]>
+ ⚠ aucune information saisie
- %d dénombrement
- %d dénombrements
- %d dénombrements
- aucun dénombrement
+ ⚠ aucun dénombrement
Saisie supprimée
Annuler
Aucune saisie. Redirection vers la dernière étape valide.
diff --git a/occtax/src/main/res/values/attrs.xml b/occtax/src/main/res/values/attrs.xml
index 9726eb32..e9d7a09d 100644
--- a/occtax/src/main/res/values/attrs.xml
+++ b/occtax/src/main/res/values/attrs.xml
@@ -1,6 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/occtax/src/main/res/values/nomenclatures.xml b/occtax/src/main/res/values/nomenclatures.xml
index 5ed885a7..e8e81f9c 100644
--- a/occtax/src/main/res/values/nomenclatures.xml
+++ b/occtax/src/main/res/values/nomenclatures.xml
@@ -1,6 +1,7 @@
+
Technical observation
Biological state
More information
@@ -10,7 +11,7 @@
Behaviour
Level of naturalness
Proof of existence
- Comment
+ Taxon comment
Life stage
Sex
Purpose of the enumeration
diff --git a/occtax/src/main/res/values/strings.xml b/occtax/src/main/res/values/strings.xml
index 4764fc88..2e49daee 100644
--- a/occtax/src/main/res/values/strings.xml
+++ b/occtax/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Sign in
Settings
+ Edit input
Dataset
Observers
Taxa filters
@@ -55,8 +56,6 @@
- %d selected
Save
- Add comment
- Edit comment
New version available
A new version of this app is available.\nWould you like to upgrade now?
@@ -69,9 +68,6 @@
Delete this input?
Delete this counting?
Delete this taxon?
- Add a comment
- Edit a comment
- Add a comment
OK
Cancel
@@ -94,7 +90,8 @@
Observers & date
Pointing
- Taxa
+ Taxa added
+ Select taxon
Information
Counting
Summary
@@ -106,19 +103,20 @@
No selected observer.\nAdd one by tapping the "Add" button.
Dataset
- Date
- Input date
- Start date
- End date
- Missing start date
- Start date should be set before now
- Missing end date
- End date should be after start date
- EEE dd MMM yyyy
- EEE dd MMM yyyy \'at\' HH:mm
- Input comment
- Add a comment
- Edit a comment
+
+ Date
+ Input date
+ Start date
+ End date
+ Missing start date
+ Start date should be set before now
+ Missing end date
+ End date should be after start date
+ EEE dd MMM yyyy
+ EEE dd MMM yyyy \'at\' HH:mm
+ Input comment
+ Add a comment
+ Edit a comment
- %d taxon found
@@ -149,9 +147,7 @@
- %d days
- More information
- Add determiner
- Add comment
+ ⚠ No nomenclature found.\nPlease start a data synchronization.
No counting.\nCreate a new one by tapping the + button.
Counting #%d
@@ -162,13 +158,18 @@
Min
Max
+
+ - %d taxon
+ - %d taxa
+
No input taxon.\nCreate a new one by tapping the + button.
%1$s:]]> %2$s]]>
+ ⚠ no information
- one counting
- %d counting
- no counting
+ ⚠ no counting
Input taxon deleted
Undo
No input taxon.\nRedirecting to the last valid step.
diff --git a/occtax/src/test/java/fr/geonature/occtax/CoroutineTestRule.kt b/occtax/src/test/java/fr/geonature/occtax/CoroutineTestRule.kt
new file mode 100644
index 00000000..dca9ac6c
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/CoroutineTestRule.kt
@@ -0,0 +1,26 @@
+package fr.geonature.occtax
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+@ExperimentalCoroutinesApi
+class CoroutineTestRule(val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler())) :
+ TestWatcher() {
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImplTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImplTest.kt
new file mode 100644
index 00000000..91eb1cdc
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/InMemoryPropertyValueLocalDataSourceImplTest.kt
@@ -0,0 +1,219 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.input.PropertyValue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [InMemoryPropertyValueLocalDataSourceImpl].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class InMemoryPropertyValueLocalDataSourceImplTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private lateinit var propertyValueLocalDataSource: IPropertyValueLocalDataSource
+
+ @Before
+ fun setUp() {
+ propertyValueLocalDataSource = InMemoryPropertyValueLocalDataSourceImpl()
+ }
+
+ @Test
+ fun `should return an empty list if no property values was set`() = runTest {
+ assertTrue(propertyValueLocalDataSource.getPropertyValues().isEmpty())
+ }
+
+ @Test
+ fun `should return a list of property values added`() = runTest {
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "DETERMINER",
+ label = null,
+ value = "some value"
+ )
+ )
+
+ assertEquals(
+ listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ ),
+ propertyValueLocalDataSource.getPropertyValues()
+ )
+ assertEquals(
+ listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ ),
+ PropertyValue(
+ code = "DETERMINER",
+ label = null,
+ value = "some value"
+ )
+ ),
+ propertyValueLocalDataSource.getPropertyValues(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `should remove a non existing property value`() = runTest {
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ propertyValueLocalDataSource.clearPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ "DETERMINER"
+ )
+ propertyValueLocalDataSource.clearPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+
+ assertEquals(
+ listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ ),
+ propertyValueLocalDataSource.getPropertyValues()
+ )
+ }
+
+ @Test
+ fun `should remove an existing property value`() = runTest {
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ ),
+ PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ )
+ )
+ propertyValueLocalDataSource.clearPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ "DETERMINER"
+ )
+
+ assertEquals(
+ listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ ),
+ propertyValueLocalDataSource.getPropertyValues()
+ )
+ }
+
+ @Test
+ fun `should clear all property values`() = runTest {
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = Taxonomy.ANY,
+ group = Taxonomy.ANY
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ ),
+ PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ )
+ )
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ propertyValueLocalDataSource.clearAllPropertyValues()
+
+ assertTrue(propertyValueLocalDataSource.getPropertyValues().isEmpty())
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceTest.kt
new file mode 100644
index 00000000..b24c7bb0
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureLocalDataSourceTest.kt
@@ -0,0 +1,417 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import fr.geonature.commons.data.LocalDatabase
+import fr.geonature.commons.data.dao.DefaultNomenclatureDao
+import fr.geonature.commons.data.dao.NomenclatureDao
+import fr.geonature.commons.data.dao.NomenclatureTaxonomyDao
+import fr.geonature.commons.data.dao.NomenclatureTypeDao
+import fr.geonature.commons.data.dao.TaxonomyDao
+import fr.geonature.commons.data.entity.DefaultNomenclature
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.NomenclatureTaxonomy
+import fr.geonature.commons.data.entity.NomenclatureType
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.occtax.CoroutineTestRule
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+
+/**
+ * Unit tests about [INomenclatureLocalDataSource].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class NomenclatureLocalDataSourceTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private lateinit var db: LocalDatabase
+ private lateinit var taxonomyDao: TaxonomyDao
+ private lateinit var nomenclatureTypeDao: NomenclatureTypeDao
+ private lateinit var nomenclatureTaxonomyDao: NomenclatureTaxonomyDao
+ private lateinit var nomenclatureDao: NomenclatureDao
+ private lateinit var defaultNomenclatureDao: DefaultNomenclatureDao
+ private lateinit var nomenclatureLocalDataSource: INomenclatureLocalDataSource
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext()
+ db = Room
+ .inMemoryDatabaseBuilder(
+ context,
+ LocalDatabase::class.java
+ )
+ .allowMainThreadQueries()
+ .build()
+ taxonomyDao = db.taxonomyDao()
+ nomenclatureTypeDao = db.nomenclatureTypeDao()
+ nomenclatureTaxonomyDao = db.nomenclatureTaxonomyDao()
+ nomenclatureDao = db.nomenclatureDao()
+ defaultNomenclatureDao = db.defaultNomenclatureDao()
+
+ nomenclatureLocalDataSource = NomenclatureLocalDataSourceImpl(
+ "occtax",
+ nomenclatureTypeDao,
+ nomenclatureDao
+ )
+ }
+
+ @After
+ @Throws(IOException::class)
+ fun tearDown() {
+ db.close()
+ }
+
+ @Test
+ fun `should find all nomenclature types`() = runTest {
+ val expectedNomenclatureTypes = initializeNomenclatureTypes()
+ val nomenclatureTypesFromDb = nomenclatureLocalDataSource.getAllNomenclatureTypes()
+
+ assertEquals(
+ expectedNomenclatureTypes.sortedBy { it.defaultLabel },
+ nomenclatureTypesFromDb
+ )
+ }
+
+ @Test
+ fun `should find all default nomenclature values`() = runTest {
+ initializeNomenclatureTypes()
+ val expectedNomenclatures = initializeNomenclaturesByTypes()
+ val expectedDefaultNomenclatureValues = initializeDefaultNomenclatureValues()
+
+ val nomenclatureTypesWithDefaultValuesFromDb =
+ nomenclatureLocalDataSource.getAllDefaultNomenclatureValues()
+
+ assertEquals(
+ expectedNomenclatures.filter { expectedDefaultNomenclatureValues.any { defaultNomenclature -> defaultNomenclature.nomenclatureId == it.id } },
+ nomenclatureTypesWithDefaultValuesFromDb
+ )
+ }
+
+ @Test
+ fun `should find nomenclatures by type with no taxonomy`() = runTest {
+ initializeTaxonomy()
+ initializeNomenclatureTypes()
+ val expectedNomenclatures = initializeNomenclaturesByTypes()
+ val expectedNomenclatureTaxonomy = initializeNomenclaturesTaxonomy()
+
+ val nomenclaturesForStatutBioAndNoTaxonomy =
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(mnemonic = "STATUT_BIO")
+
+ assertEquals(expectedNomenclatureTaxonomy
+ .filter { it.taxonomy == Taxonomy(kingdom = Taxonomy.ANY) }
+ .mapNotNull { expectedNomenclatures.find { nomenclature -> nomenclature.id == it.nomenclatureId } }
+ .sortedBy { it.defaultLabel },
+ nomenclaturesForStatutBioAndNoTaxonomy
+ )
+ }
+
+ @Test
+ fun `should find nomenclatures by type with any taxonomy`() = runTest {
+ initializeTaxonomy()
+ initializeNomenclatureTypes()
+ val expectedNomenclatures = initializeNomenclaturesByTypes()
+ val expectedNomenclatureTaxonomy = initializeNomenclaturesTaxonomy()
+
+ val nomenclaturesForStatutBioAndAnyKingdomTaxonomy =
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(kingdom = Taxonomy.ANY)
+ )
+
+ assertEquals(expectedNomenclatureTaxonomy
+ .filter { it.taxonomy == Taxonomy(kingdom = Taxonomy.ANY) }
+ .mapNotNull { expectedNomenclatures.find { nomenclature -> nomenclature.id == it.nomenclatureId } }
+ .sortedBy { it.defaultLabel },
+ nomenclaturesForStatutBioAndAnyKingdomTaxonomy
+ )
+ }
+
+ @Test
+ fun `should find nomenclatures by type matching given taxonomy kingdom`() = runTest {
+ initializeTaxonomy()
+ initializeNomenclatureTypes()
+ val expectedNomenclatures = initializeNomenclaturesByTypes()
+ val expectedNomenclatureTaxonomy = initializeNomenclaturesTaxonomy()
+
+ val nomenclaturesForStatutBioAndAnyTaxonomy =
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(kingdom = "Animalia")
+ )
+
+ assertEquals(expectedNomenclatureTaxonomy
+ .filter {
+ listOf(
+ Taxonomy(kingdom = Taxonomy.ANY),
+ Taxonomy(kingdom = "Animalia")
+ ).contains(it.taxonomy)
+ }
+ .mapNotNull { expectedNomenclatures.find { nomenclature -> nomenclature.id == it.nomenclatureId } }
+ .sortedBy { it.defaultLabel },
+ nomenclaturesForStatutBioAndAnyTaxonomy
+ )
+ }
+
+ @Test
+ fun `should find nomenclatures by type matching given taxonomy kingdom and group`() = runTest {
+ initializeTaxonomy()
+ initializeNomenclatureTypes()
+ val expectedNomenclatures = initializeNomenclaturesByTypes()
+ val expectedNomenclatureTaxonomy = initializeNomenclaturesTaxonomy()
+
+ val nomenclaturesForStatutBioAndAnyTaxonomy =
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+
+ assertEquals(expectedNomenclatureTaxonomy
+ .filter {
+ listOf(
+ Taxonomy(kingdom = Taxonomy.ANY),
+ Taxonomy(kingdom = "Animalia"),
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ).contains(it.taxonomy)
+ }
+ .mapNotNull { expectedNomenclatures.find { nomenclature -> nomenclature.id == it.nomenclatureId } }
+ .sortedBy { it.defaultLabel },
+ nomenclaturesForStatutBioAndAnyTaxonomy
+ )
+ }
+
+ private fun initializeTaxonomy(): List {
+ val expectedTaxonomy = listOf(
+ Taxonomy(kingdom = Taxonomy.ANY),
+ Taxonomy(kingdom = "Animalia"),
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Amphibiens"
+ ),
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Mammifères"
+ ),
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Reptiles"
+ ),
+ Taxonomy(kingdom = "Fungi"),
+ Taxonomy(kingdom = "Plantae")
+ )
+
+ taxonomyDao.insert(*expectedTaxonomy.toTypedArray())
+
+ return expectedTaxonomy
+ }
+
+ private fun initializeNomenclatureTypes(): List {
+ val expectedNomenclatureTypes = listOf(
+ NomenclatureType(
+ id = 7,
+ mnemonic = "ETA_BIO",
+ defaultLabel = "Etat biologique de l'observation"
+ ),
+ NomenclatureType(
+ id = 13,
+ mnemonic = "STATUT_BIO",
+ defaultLabel = "Statut biologique"
+ ),
+ NomenclatureType(
+ id = 14,
+ mnemonic = "METH_OBS",
+ defaultLabel = "Méthodes d'observation"
+ )
+ )
+
+ nomenclatureTypeDao.insert(*expectedNomenclatureTypes.toTypedArray())
+
+ return expectedNomenclatureTypes
+ }
+
+ private fun initializeNomenclaturesByTypes(): List {
+ val expectedNomenclatures = listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 31,
+ code = "3",
+ hierarchy = "013.003",
+ defaultLabel = "Reproduction",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 32,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 33,
+ code = "5",
+ hierarchy = "013.005",
+ defaultLabel = "Estivation",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 157,
+ code = "1",
+ hierarchy = "007.001",
+ defaultLabel = "Non renseigné",
+ typeId = 7
+ ),
+ Nomenclature(
+ id = 158,
+ code = "2",
+ hierarchy = "007.002",
+ defaultLabel = "Observé vivant",
+ typeId = 7
+ ),
+ Nomenclature(
+ id = 159,
+ code = "3",
+ hierarchy = "007.003",
+ defaultLabel = "Trouvé mort",
+ typeId = 7
+ ),
+ Nomenclature(
+ id = 160,
+ code = "4",
+ hierarchy = "007.004",
+ defaultLabel = "Indice de présence",
+ typeId = 7
+ ),
+ Nomenclature(
+ id = 161,
+ code = "5",
+ hierarchy = "007.005",
+ defaultLabel = "Issu d'élevage",
+ typeId = 7
+ )
+ )
+
+ nomenclatureDao.insert(*expectedNomenclatures.toTypedArray())
+
+ return expectedNomenclatures
+ }
+
+ private fun initializeNomenclaturesTaxonomy(): List {
+ val expectedNomenclatureTaxonomy = listOf(
+ NomenclatureTaxonomy(
+ nomenclatureId = 29,
+ taxonomy = Taxonomy(kingdom = Taxonomy.ANY)
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 31,
+ taxonomy = Taxonomy(kingdom = "Animalia")
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 31,
+ taxonomy = Taxonomy(kingdom = "Fungi")
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 31,
+ taxonomy = Taxonomy(kingdom = "Plantae")
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 32,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Amphibiens"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 32,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Mammifères"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 32,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 32,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Reptiles"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 33,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Mammifères"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 33,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ),
+ NomenclatureTaxonomy(
+ nomenclatureId = 33,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Reptiles"
+ )
+ )
+ )
+
+ nomenclatureTaxonomyDao.insert(*expectedNomenclatureTaxonomy.toTypedArray())
+
+ return expectedNomenclatureTaxonomy
+ }
+
+ private fun initializeDefaultNomenclatureValues(): List {
+ val expectedDefaultNomenclatureValues = listOf(
+ DefaultNomenclature(
+ "occtax",
+ 29
+ )
+ )
+
+ defaultNomenclatureDao.insert(*expectedDefaultNomenclatureValues.toTypedArray())
+
+ return expectedDefaultNomenclatureValues
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceTest.kt
new file mode 100644
index 00000000..f9c8e428
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/data/NomenclatureSettingsLocalDataSourceTest.kt
@@ -0,0 +1,198 @@
+package fr.geonature.occtax.features.nomenclature.data
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.settings.PropertySettings
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [INomenclatureSettingsLocalDataSource].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class NomenclatureSettingsLocalDataSourceTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private lateinit var nomenclatureSettingsLocalDataSource: INomenclatureSettingsLocalDataSource
+
+ @Before
+ fun setUp() {
+ nomenclatureSettingsLocalDataSource = NomenclatureSettingsLocalDataSourceImpl()
+ }
+
+ @Test
+ fun `should get default nomenclature type settings by nomenclature main type`() = runTest {
+ assertEquals(
+ listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_DETERMIN",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "OCC_COMPORTEMENT",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "NATURALITE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "PREUVE_EXIST",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "COMMENT",
+ BaseEditableNomenclatureType.ViewType.TEXT_MULTIPLE,
+ default = false
+ )
+ ),
+ nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION)
+ )
+
+ assertEquals(
+ listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "STADE_VIE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "SEXE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "OBJ_DENBR",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "TYP_DENBR",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "MIN",
+ BaseEditableNomenclatureType.ViewType.MIN_MAX
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "MAX",
+ BaseEditableNomenclatureType.ViewType.MIN_MAX
+ )
+ ),
+ nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.COUNTING)
+ )
+ }
+
+ @Test
+ fun `should get nomenclature type settings by nomenclature main type according to given settings`() =
+ runTest {
+ assertEquals(
+ listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = true,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "OCC_COMPORTEMENT",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = false
+ )
+ ),
+ nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ PropertySettings(
+ key = "METH_OBS",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ key = "ETA_BIO",
+ visible = true,
+ default = false
+ ),
+ PropertySettings(
+ key = "OCC_COMPORTEMENT",
+ visible = false,
+ default = true
+ )
+ )
+ )
+
+ assertEquals(
+ listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "STADE_VIE",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ )
+ ),
+ nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ PropertySettings(
+ key = "STADE_VIE",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ key = "NO_SUCH_SETTINGS",
+ visible = true,
+ default = true
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureTypeTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureTypeTest.kt
new file mode 100644
index 00000000..a8a5ce6d
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/domain/EditableNomenclatureTypeTest.kt
@@ -0,0 +1,130 @@
+package fr.geonature.occtax.features.nomenclature.domain
+
+import android.os.Bundle
+import android.os.Parcel
+import fr.geonature.occtax.input.PropertyValue
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Unit tests about [EditableNomenclatureType].
+ *
+ * @author S. Grimault
+ */
+@RunWith(RobolectricTestRunner::class)
+class EditableNomenclatureTypeTest {
+
+ @Test
+ fun `should be the same editable nomenclature type`() {
+ assertEquals(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = true,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = true,
+ default = false
+ ),
+ )
+
+ assertEquals(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ default = false
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ default = false
+ )
+ )
+ }
+
+ @Test
+ fun `should create EditableNomenclatureType from Parcelable`() {
+ // given an editable nomenclature type instance
+ val editableNomenclatureType = EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ default = false,
+ locked = true
+ )
+
+ // when we obtain a Parcel object to write the editable nomenclature type instance to it
+ val parcel = Parcel.obtain()
+ editableNomenclatureType.writeToParcel(
+ parcel,
+ 0
+ )
+
+ // reset the parcel for reading
+ parcel.setDataPosition(0)
+
+ // then
+ assertEquals(
+ editableNomenclatureType,
+ EditableNomenclatureType.CREATOR.createFromParcel(parcel)
+ )
+ }
+
+ @Test
+ fun `should create a list of EditableNomenclatureType from Parcelable array`() {
+ // given a list of editable nomenclature types
+ val expectedEditableNomenclatureTypes = listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ default = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ ),
+ locked = true
+ )
+ )
+
+ // when creating a bundle of them
+ val bundle = Bundle().apply {
+ putParcelableArray(
+ "editable_nomenclature_types",
+ expectedEditableNomenclatureTypes.toTypedArray()
+ )
+ }
+
+ // then
+ assertArrayEquals(
+ expectedEditableNomenclatureTypes.toTypedArray(),
+ bundle.getParcelableArray("editable_nomenclature_types")
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModelTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModelTest.kt
new file mode 100644
index 00000000..dae43e40
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/presentation/NomenclatureViewModelTest.kt
@@ -0,0 +1,278 @@
+package fr.geonature.occtax.features.nomenclature.presentation
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Observer
+import androidx.lifecycle.viewModelScope
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.error.Failure
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureTypeFoundLocallyFailure
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureValuesFoundFailure
+import fr.geonature.occtax.features.nomenclature.usecase.GetEditableNomenclaturesUseCase
+import fr.geonature.occtax.features.nomenclature.usecase.GetNomenclatureValuesByTypeAndTaxonomyUseCase
+import fr.geonature.occtax.input.PropertyValue
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.confirmVerified
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.unmockkAll
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [NomenclatureViewModel].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class NomenclatureViewModelTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @RelaxedMockK
+ private lateinit var getEditableNomenclaturesUseCase: GetEditableNomenclaturesUseCase
+
+ @MockK
+ private lateinit var getNomenclatureValuesByTypeAndTaxonomyUseCase: GetNomenclatureValuesByTypeAndTaxonomyUseCase
+
+ @RelaxedMockK
+ private lateinit var editableNomenclaturesObserver: Observer>
+
+ @RelaxedMockK
+ private lateinit var nomenclatureValuesObserver: Observer>
+
+ @RelaxedMockK
+ private lateinit var failureObserver: Observer
+
+ private lateinit var nomenclatureViewModel: NomenclatureViewModel
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ nomenclatureViewModel = NomenclatureViewModel(
+ getEditableNomenclaturesUseCase,
+ getNomenclatureValuesByTypeAndTaxonomyUseCase
+ )
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `should get all editable nomenclature types with default value by nomenclature main type`() =
+ runTest {
+ // given some nomenclature types with default values
+ val expectedEditableNomenclatures = listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ )
+ coEvery {
+ getEditableNomenclaturesUseCase.run(
+ GetEditableNomenclaturesUseCase.Params(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ )
+ )
+ } returns Right(expectedEditableNomenclatures)
+ coEvery {
+ getEditableNomenclaturesUseCase(
+ GetEditableNomenclaturesUseCase.Params(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ ),
+ nomenclatureViewModel.viewModelScope,
+ any()
+ )
+ } answers { callOriginal() }
+
+ // when
+ nomenclatureViewModel.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+ nomenclatureViewModel.editableNomenclatures.observeForever(editableNomenclaturesObserver)
+ nomenclatureViewModel.failure.observeForever(failureObserver)
+
+ // then
+ verify(atLeast = 1) { editableNomenclaturesObserver.onChanged(expectedEditableNomenclatures) }
+ confirmVerified(editableNomenclaturesObserver)
+ }
+
+ @Test
+ fun `should get NoNomenclatureTypeFoundLocallyFailure if no nomenclature types was found`() =
+ runTest {
+ // given some failure from usecase
+ coEvery {
+ getEditableNomenclaturesUseCase.run(
+ GetEditableNomenclaturesUseCase.Params(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ )
+ )
+ } returns Left(NoNomenclatureTypeFoundLocallyFailure)
+ coEvery {
+ getEditableNomenclaturesUseCase(
+ GetEditableNomenclaturesUseCase.Params(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ ),
+ nomenclatureViewModel.viewModelScope,
+ any()
+ )
+ } answers { callOriginal() }
+
+ // when
+ nomenclatureViewModel.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+ nomenclatureViewModel.editableNomenclatures.observeForever(editableNomenclaturesObserver)
+ nomenclatureViewModel.failure.observeForever(failureObserver)
+
+ // then
+ verify { failureObserver.onChanged(NoNomenclatureTypeFoundLocallyFailure) }
+ confirmVerified(failureObserver)
+ }
+
+ @Test
+ fun `should get nomenclature values by type matching given taxonomy kingdom and group`() =
+ runTest {
+ // given some nomenclature values from given type
+ val expectedNomenclatureValues = listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 31,
+ code = "3",
+ hierarchy = "013.003",
+ defaultLabel = "Reproduction",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 32,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ )
+ )
+ // from usecase
+ coEvery {
+ getNomenclatureValuesByTypeAndTaxonomyUseCase.run(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+ } returns Right(expectedNomenclatureValues)
+ coEvery {
+ getNomenclatureValuesByTypeAndTaxonomyUseCase(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ),
+ nomenclatureViewModel.viewModelScope,
+ any()
+ )
+ } answers { callOriginal() }
+
+ // when
+ nomenclatureViewModel.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ).observeForever(nomenclatureValuesObserver)
+ nomenclatureViewModel.failure.observeForever(failureObserver)
+
+ // then
+ verify(atLeast = 1) { nomenclatureValuesObserver.onChanged(expectedNomenclatureValues) }
+ confirmVerified(nomenclatureValuesObserver)
+ }
+
+ @Test
+ fun `should return NoNomenclatureValuesFoundFailure if no nomenclature values was found from given type`() =
+ runTest {
+ // given some failure from usecase
+ coEvery {
+ getNomenclatureValuesByTypeAndTaxonomyUseCase.run(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+ } returns Left(NoNomenclatureValuesFoundFailure("STATUT_BIO"))
+ coEvery {
+ getNomenclatureValuesByTypeAndTaxonomyUseCase(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ),
+ nomenclatureViewModel.viewModelScope,
+ any()
+ )
+ } answers { callOriginal() }
+
+ // when
+ nomenclatureViewModel.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ ).observeForever(nomenclatureValuesObserver)
+ nomenclatureViewModel.failure.observeForever(failureObserver)
+
+ // then
+ verify(atLeast = 1) { failureObserver.onChanged(NoNomenclatureValuesFoundFailure("STATUT_BIO")) }
+ confirmVerified(failureObserver)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryTest.kt
new file mode 100644
index 00000000..9570dd45
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/DefaultPropertyValueRepositoryTest.kt
@@ -0,0 +1,251 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.orNull
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureLocalDataSource
+import fr.geonature.occtax.features.nomenclature.data.IPropertyValueLocalDataSource
+import fr.geonature.occtax.input.PropertyValue
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.impl.annotations.MockK
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [IDefaultPropertyValueRepository].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class DefaultPropertyValueRepositoryTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var propertyValueLocalDataSource: IPropertyValueLocalDataSource
+
+ @MockK
+ private lateinit var nomenclatureLocalDataSource: INomenclatureLocalDataSource
+
+ private lateinit var defaultPropertyValueRepository: IDefaultPropertyValueRepository
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ defaultPropertyValueRepository =
+ DefaultPropertyValueRepositoryImpl(
+ propertyValueLocalDataSource,
+ nomenclatureLocalDataSource
+ )
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `should return an empty list if no property values was set`() = runTest {
+ // given no property values added
+ coEvery { propertyValueLocalDataSource.getPropertyValues() } returns listOf()
+
+ // when
+ val result = defaultPropertyValueRepository.getPropertyValues()
+
+ // then
+ assertTrue(result.isRight)
+ assertTrue(result.orNull()?.isEmpty() == true)
+ }
+
+ @Test
+ fun `should return a list of property values added`() = runTest {
+ // given some property values added
+ val expectedPropertyValues = listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ ),
+ PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ )
+ )
+ coEvery { propertyValueLocalDataSource.getPropertyValues() } returns expectedPropertyValues
+ coEvery { nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy("STATUT_BIO") } returns listOf()
+ coEvery { nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy("DETERMINER") } returns listOf()
+
+ // when
+ val result = defaultPropertyValueRepository.getPropertyValues()
+
+ // then
+ assertTrue(result.isRight)
+ assertEquals(
+ expectedPropertyValues,
+ result.orNull()
+ )
+ }
+
+ @Test
+ fun `should return a list of property values added matching given taxonomy rank`() = runTest {
+ // given some property values added
+ val expectedPropertyValues = listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ ),
+ PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ )
+ )
+ coEvery {
+ propertyValueLocalDataSource.getPropertyValues(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns expectedPropertyValues
+ coEvery {
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns listOf(
+ Nomenclature(
+ id = 33,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 34,
+ code = "5",
+ hierarchy = "013.005",
+ defaultLabel = "Estivation",
+ typeId = 13
+ )
+ )
+ coEvery {
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ "DETERMINER",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns listOf()
+
+ // when
+ val result = defaultPropertyValueRepository.getPropertyValues(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+
+ // then
+ assertTrue(result.isRight)
+ assertEquals(
+ expectedPropertyValues,
+ result.orNull()
+ )
+ }
+
+ @Test
+ fun `should set new property value`() = runTest {
+ coEvery {
+ propertyValueLocalDataSource.setPropertyValue(
+ any(),
+ any()
+ )
+ } returns Unit
+
+ // when
+ val result = defaultPropertyValueRepository.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+
+ // then
+ assertTrue(result.isRight)
+ coVerify {
+ propertyValueLocalDataSource.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ }
+ confirmVerified(propertyValueLocalDataSource)
+ }
+
+ @Test
+ fun `should clear existing property value`() = runTest {
+ coEvery {
+ propertyValueLocalDataSource.clearPropertyValue(
+ any(),
+ any()
+ )
+ } returns Unit
+
+ // when
+ val result = defaultPropertyValueRepository.clearPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+
+ // then
+ assertTrue(result.isRight)
+ coVerify {
+ propertyValueLocalDataSource.clearPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+ }
+ confirmVerified(propertyValueLocalDataSource)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryTest.kt
new file mode 100644
index 00000000..9e0efcb1
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/repository/NomenclatureRepositoryTest.kt
@@ -0,0 +1,518 @@
+package fr.geonature.occtax.features.nomenclature.repository
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.NomenclatureType
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.identity
+import fr.geonature.commons.fp.orNull
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureLocalDataSource
+import fr.geonature.occtax.features.nomenclature.data.INomenclatureSettingsLocalDataSource
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureTypeFoundLocallyFailure
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureValuesFoundFailure
+import fr.geonature.occtax.input.PropertyValue
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [INomenclatureRepository].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class NomenclatureRepositoryTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var nomenclatureLocalDataSource: INomenclatureLocalDataSource
+
+ @MockK
+ private lateinit var nomenclatureSettingsLocalDataSource: INomenclatureSettingsLocalDataSource
+
+ private lateinit var nomenclatureRepository: INomenclatureRepository
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ nomenclatureRepository = NomenclatureRepositoryImpl(
+ nomenclatureLocalDataSource,
+ nomenclatureSettingsLocalDataSource
+ )
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `should get default nomenclature type settings by nomenclature main type`() = runTest {
+ // given no nomenclature types found
+ coEvery { nomenclatureLocalDataSource.getAllNomenclatureTypes() } returns listOf(
+ NomenclatureType(
+ id = 7,
+ mnemonic = "ETA_BIO",
+ defaultLabel = "Etat biologique de l'observation"
+ ),
+ NomenclatureType(
+ id = 13,
+ mnemonic = "STATUT_BIO",
+ defaultLabel = "Statut biologique"
+ ),
+ NomenclatureType(
+ id = 14,
+ mnemonic = "METH_OBS",
+ defaultLabel = "Méthodes d'observation"
+ )
+ )
+ // and corresponding editable nomenclature types
+ coEvery { nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION) } returns listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ true
+ ),
+ BaseEditableNomenclatureType.from(
+
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ true
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = false,
+ default = false
+ ),
+ )
+ // and some default values for these types
+ coEvery { nomenclatureLocalDataSource.getAllDefaultNomenclatureValues() } returns listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ )
+
+ // when
+ val editableNomenclatureSettings =
+ nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+
+ // then
+ assertTrue(editableNomenclatureSettings.isRight)
+ assertEquals(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ default = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ ),
+ editableNomenclatureSettings.orNull()
+ )
+ }
+
+ @Test
+ fun `should get nomenclature type settings with no default nomenclature values if no corresponding nomenclature types was found`() =
+ runTest {
+ // given no nomenclature types found
+ coEvery { nomenclatureLocalDataSource.getAllNomenclatureTypes() } returns listOf(
+ NomenclatureType(
+ id = 7,
+ mnemonic = "ETA_BIO",
+ defaultLabel = "Etat biologique de l'observation"
+ ),
+ NomenclatureType(
+ id = 14,
+ mnemonic = "METH_OBS",
+ defaultLabel = "Méthodes d'observation"
+ )
+ )
+ // and corresponding editable nomenclature types
+ coEvery { nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION) } returns listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ true
+ ),
+ BaseEditableNomenclatureType.from(
+
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ true
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = false,
+ default = false
+ ),
+ )
+ // and some default values for these types
+ coEvery { nomenclatureLocalDataSource.getAllDefaultNomenclatureValues() } returns listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ )
+
+ // when
+ val editableNomenclatureSettings =
+ nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+
+ // then
+ assertTrue(editableNomenclatureSettings.isRight)
+ assertEquals(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ )
+ ),
+ editableNomenclatureSettings.orNull()
+ )
+ }
+
+ @Test
+ fun `should get other default nomenclature type settings even if no nomenclature types was found`() =
+ runTest {
+ // given no nomenclature types found
+ coEvery { nomenclatureLocalDataSource.getAllNomenclatureTypes() } returns listOf()
+ // and some editable nomenclature types
+ coEvery { nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION) } returns listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ default = false
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "COMMENT",
+ BaseEditableNomenclatureType.ViewType.TEXT_MULTIPLE,
+ visible = true,
+ default = false
+ )
+ )
+ // and no default values for these types
+ coEvery { nomenclatureLocalDataSource.getAllDefaultNomenclatureValues() } returns listOf()
+
+ // when
+ val editableNomenclatureSettings =
+ nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+
+ // then
+ assertTrue(editableNomenclatureSettings.isRight)
+ assertEquals(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "COMMENT",
+ BaseEditableNomenclatureType.ViewType.TEXT_MULTIPLE,
+ visible = true,
+ default = false
+ )
+ ),
+ editableNomenclatureSettings.orNull()
+ )
+ }
+
+ @Test
+ fun `should return NoNomenclatureTypeFoundLocallyFailure if no nomenclature types was found`() =
+ runTest {
+ // given no nomenclature types found
+ coEvery { nomenclatureLocalDataSource.getAllNomenclatureTypes() } returns listOf()
+ // and some editable nomenclature types
+ coEvery { nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION) } returns listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ ),
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ visible = false,
+ default = false
+ ),
+ )
+ // and no default values for these types
+ coEvery { nomenclatureLocalDataSource.getAllDefaultNomenclatureValues() } returns listOf()
+
+ // when
+ val editableNomenclatureSettings =
+ nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+
+ // then
+ assertTrue(editableNomenclatureSettings.isLeft)
+ assertTrue(editableNomenclatureSettings.fold(::identity) {} is NoNomenclatureTypeFoundLocallyFailure)
+ }
+
+ @Test
+ fun `should return NoNomenclatureTypeFoundLocallyFailure if no nomenclature type settings matches nomenclature types`() =
+ runTest {
+ // given some nomenclature types
+ coEvery { nomenclatureLocalDataSource.getAllNomenclatureTypes() } returns listOf(
+ NomenclatureType(
+ id = 7,
+ mnemonic = "ETA_BIO",
+ defaultLabel = "Etat biologique de l'observation"
+ ),
+ NomenclatureType(
+ id = 13,
+ mnemonic = "STATUT_BIO",
+ defaultLabel = "Statut biologique"
+ ),
+ NomenclatureType(
+ id = 14,
+ mnemonic = "METH_OBS",
+ defaultLabel = "Méthodes d'observation"
+ )
+ )
+ // and some other editable nomenclature types
+ coEvery { nomenclatureSettingsLocalDataSource.getNomenclatureTypeSettings(BaseEditableNomenclatureType.Type.INFORMATION) } returns listOf(
+ BaseEditableNomenclatureType.from(
+ BaseEditableNomenclatureType.Type.COUNTING,
+ "TYP_DENBR",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE
+ )
+ )
+ // and some default values for these types
+ coEvery { nomenclatureLocalDataSource.getAllDefaultNomenclatureValues() } returns listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ )
+
+ // when
+ val editableNomenclatureSettings =
+ nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION)
+
+ // then
+ assertTrue(editableNomenclatureSettings.isLeft)
+ assertTrue(editableNomenclatureSettings.fold(::identity) {} is NoNomenclatureTypeFoundLocallyFailure)
+ }
+
+ @Test
+ fun `should get nomenclature values by type matching given taxonomy kingdom and group`() =
+ runTest {
+ coEvery {
+ // given some nomenclature values from given type
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 31,
+ code = "3",
+ hierarchy = "013.003",
+ defaultLabel = "Reproduction",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 32,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ )
+ )
+
+ // when
+ val nomenclatures = nomenclatureRepository.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+
+ // then
+ assertTrue(nomenclatures.isRight)
+ assertEquals(
+ listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 31,
+ code = "3",
+ hierarchy = "013.003",
+ defaultLabel = "Reproduction",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 32,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ )
+ ),
+ nomenclatures.orNull()
+ )
+ }
+
+ @Test
+ fun `should return NoNomenclatureValuesFoundFailure if no nomenclature values was found from given type`() =
+ runTest {
+ // given no nomenclature values from given type
+ coEvery {
+ nomenclatureLocalDataSource.getNomenclatureValuesByTypeAndTaxonomy(
+ "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns emptyList()
+
+ // when
+ val nomenclatures = nomenclatureRepository.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+
+ // then
+ assertTrue(nomenclatures.isLeft)
+ assertEquals(
+ nomenclatures.fold(::identity) {},
+ NoNomenclatureValuesFoundFailure("STATUT_BIO")
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCaseTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCaseTest.kt
new file mode 100644
index 00000000..e660d857
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/ClearDefaultPropertyValueUseCaseTest.kt
@@ -0,0 +1,119 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.commons.fp.identity
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.error.PropertyValueFailure
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [ClearDefaultPropertyValueUseCase].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class ClearDefaultPropertyValueUseCaseTest {
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var defaultPropertyValueRepository: IDefaultPropertyValueRepository
+
+ private lateinit var clearDefaultPropertyValueUseCase: ClearDefaultPropertyValueUseCase
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ clearDefaultPropertyValueUseCase =
+ ClearDefaultPropertyValueUseCase(defaultPropertyValueRepository)
+ }
+
+ @Test
+ fun `should clear existing property value`() = runTest {
+ coEvery {
+ defaultPropertyValueRepository.clearPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+ } returns Right(Unit)
+
+ // when remove existing property value
+ val result = clearDefaultPropertyValueUseCase.run(
+ ClearDefaultPropertyValueUseCase.Params.Params(
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+ )
+
+ // then
+ assertTrue(result.isRight)
+ }
+
+ @Test
+ fun `should clear all property values`() = runTest {
+ coEvery {
+ defaultPropertyValueRepository.clearAllPropertyValues()
+ } returns Right(Unit)
+
+ // when remove existing property value
+ val result =
+ clearDefaultPropertyValueUseCase.run(ClearDefaultPropertyValueUseCase.Params.None)
+
+ // then
+ assertTrue(result.isRight)
+ }
+
+ @Test
+ fun `should return PropertyValueFailure if something goes wrong`() = runTest {
+ coEvery {
+ defaultPropertyValueRepository.clearPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+ } returns Left(PropertyValueFailure("STATUT_BIO"))
+
+ // when remove a property value
+ val result = clearDefaultPropertyValueUseCase.run(
+ ClearDefaultPropertyValueUseCase.Params.Params(
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ "STATUT_BIO"
+ )
+ )
+
+ // then
+ assertTrue(result.isLeft)
+ assertEquals(
+ result.fold(::identity) {},
+ PropertyValueFailure("STATUT_BIO")
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCaseTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCaseTest.kt
new file mode 100644
index 00000000..6bee1343
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetEditableNomenclaturesUseCaseTest.kt
@@ -0,0 +1,264 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.commons.fp.identity
+import fr.geonature.commons.fp.orNull
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.domain.BaseEditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.domain.EditableNomenclatureType
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureTypeFoundLocallyFailure
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import fr.geonature.occtax.features.nomenclature.repository.INomenclatureRepository
+import fr.geonature.occtax.input.PropertyValue
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [GetEditableNomenclaturesUseCase].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class GetEditableNomenclaturesUseCaseTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var nomenclatureRepository: INomenclatureRepository
+
+ @MockK
+ private lateinit var defaultPropertyValueRepository: IDefaultPropertyValueRepository
+
+ private lateinit var getEditableNomenclaturesUseCase: GetEditableNomenclaturesUseCase
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ getEditableNomenclaturesUseCase = GetEditableNomenclaturesUseCase(
+ nomenclatureRepository,
+ defaultPropertyValueRepository
+ )
+ }
+
+ @Test
+ fun `should get all editable nomenclature types with default value by nomenclature main type`() =
+ runTest {
+ // given some nomenclature types
+ coEvery { nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION) } returns Right(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ )
+ )
+ // and no default property values
+ coEvery { defaultPropertyValueRepository.getPropertyValues() } returns Right(listOf())
+
+ // when fetching all editable nomenclature types with default value
+ val response =
+ getEditableNomenclaturesUseCase.run(GetEditableNomenclaturesUseCase.Params(BaseEditableNomenclatureType.Type.INFORMATION))
+
+ // then
+ assertEquals(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ ).sortedBy { it.code },
+ response.orNull()?.sortedBy { it.code }
+ )
+ }
+
+ @Test
+ fun `should get all editable nomenclature types with some property values defined by nomenclature main type`() =
+ runTest {
+ // given some nomenclature types
+ coEvery { nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION) } returns Right(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Non renseigné",
+ value = 29L
+ )
+ )
+ )
+ )
+ // and some default property values matching taxonomy
+ coEvery {
+ defaultPropertyValueRepository.getPropertyValues(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns Right(
+ listOf(
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ ),
+ PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ )
+ )
+ )
+
+ // when fetching all editable nomenclature types with values matching given taxonomy
+ val response =
+ getEditableNomenclaturesUseCase.run(
+ GetEditableNomenclaturesUseCase.Params(
+ type = BaseEditableNomenclatureType.Type.INFORMATION,
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+
+ // then
+ assertEquals(
+ listOf(
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "METH_OBS",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Méthodes d'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "ETA_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Etat biologique de l'observation"
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "DETERMINER",
+ BaseEditableNomenclatureType.ViewType.TEXT_SIMPLE,
+ visible = true,
+ default = false,
+ value = PropertyValue(
+ "DETERMINER",
+ null,
+ "some_value"
+ ),
+ locked = true
+ ),
+ EditableNomenclatureType(
+ BaseEditableNomenclatureType.Type.INFORMATION,
+ "STATUT_BIO",
+ BaseEditableNomenclatureType.ViewType.NOMENCLATURE_TYPE,
+ label = "Statut biologique",
+ visible = false,
+ value = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ ),
+ locked = true
+ )
+ ).sortedBy { it.code },
+ response.orNull()?.sortedBy { it.code }
+ )
+ }
+
+ @Test
+ fun `should return NoNomenclatureTypeFoundLocallyFailure if no nomenclature types was found`() =
+ runTest {
+ // given some failure from repository
+ coEvery { nomenclatureRepository.getEditableNomenclatures(BaseEditableNomenclatureType.Type.INFORMATION) } returns Left(NoNomenclatureTypeFoundLocallyFailure)
+
+ // when fetching all editable nomenclature types with default value
+ val response =
+ getEditableNomenclaturesUseCase.run(GetEditableNomenclaturesUseCase.Params(BaseEditableNomenclatureType.Type.INFORMATION))
+
+ // then
+ assertTrue(response.isLeft)
+ assertTrue(response.fold(::identity) {} is NoNomenclatureTypeFoundLocallyFailure)
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCaseTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCaseTest.kt
new file mode 100644
index 00000000..71cdacc5
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/GetNomenclatureValuesByTypeAndTaxonomyUseCaseTest.kt
@@ -0,0 +1,137 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Nomenclature
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.commons.fp.identity
+import fr.geonature.commons.fp.orNull
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.error.NoNomenclatureValuesFoundFailure
+import fr.geonature.occtax.features.nomenclature.repository.INomenclatureRepository
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [GetNomenclatureValuesByTypeAndTaxonomyUseCase].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class GetNomenclatureValuesByTypeAndTaxonomyUseCaseTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var nomenclatureRepository: INomenclatureRepository
+
+ private lateinit var getNomenclatureValuesByTypeAndTaxonomyUseCase: GetNomenclatureValuesByTypeAndTaxonomyUseCase
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ getNomenclatureValuesByTypeAndTaxonomyUseCase =
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase(nomenclatureRepository)
+ }
+
+ @Test
+ fun `should get nomenclature values by type matching given taxonomy kingdom and group`() = runTest {
+ // given some nomenclature values from given type
+ val expectedNomenclatureValues = listOf(
+ Nomenclature(
+ id = 29,
+ code = "1",
+ hierarchy = "013.001",
+ defaultLabel = "Non renseigné",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 31,
+ code = "3",
+ hierarchy = "013.003",
+ defaultLabel = "Reproduction",
+ typeId = 13
+ ),
+ Nomenclature(
+ id = 32,
+ code = "4",
+ hierarchy = "013.004",
+ defaultLabel = "Hibernation",
+ typeId = 13
+ )
+ )
+ coEvery {
+ nomenclatureRepository.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns Right(expectedNomenclatureValues)
+
+ // when getting all nomenclature values
+ val response = getNomenclatureValuesByTypeAndTaxonomyUseCase.run(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+
+ // then
+ assertEquals(
+ expectedNomenclatureValues,
+ response.orNull()
+ )
+ }
+
+ @Test
+ fun `should return NoNomenclatureValuesFoundFailure if no nomenclature values was found from given type`() =
+ runTest {
+ // given some failure from repository
+ coEvery {
+ nomenclatureRepository.getNomenclatureValuesByTypeAndTaxonomy(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ } returns Left(NoNomenclatureValuesFoundFailure("STATUT_BIO"))
+
+ // when getting all nomenclature values
+ val response = getNomenclatureValuesByTypeAndTaxonomyUseCase.run(
+ GetNomenclatureValuesByTypeAndTaxonomyUseCase.Params(
+ mnemonic = "STATUT_BIO",
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ )
+ )
+ )
+
+ // then
+ assertTrue(response.isLeft)
+ assertEquals(
+ response.fold(::identity) {},
+ NoNomenclatureValuesFoundFailure("STATUT_BIO")
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCaseTest.kt b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCaseTest.kt
new file mode 100644
index 00000000..badbe0f6
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/features/nomenclature/usecase/SetDefaultPropertyValueUseCaseTest.kt
@@ -0,0 +1,123 @@
+package fr.geonature.occtax.features.nomenclature.usecase
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import fr.geonature.commons.data.entity.Taxonomy
+import fr.geonature.commons.fp.Either.Left
+import fr.geonature.commons.fp.Either.Right
+import fr.geonature.commons.fp.identity
+import fr.geonature.occtax.CoroutineTestRule
+import fr.geonature.occtax.features.nomenclature.error.PropertyValueFailure
+import fr.geonature.occtax.features.nomenclature.repository.IDefaultPropertyValueRepository
+import fr.geonature.occtax.input.PropertyValue
+import io.mockk.MockKAnnotations.init
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests about [SetDefaultPropertyValueUseCase].
+ *
+ * @author S. Grimault
+ */
+@ExperimentalCoroutinesApi
+class SetDefaultPropertyValueUseCaseTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ @MockK
+ private lateinit var defaultPropertyValueRepository: IDefaultPropertyValueRepository
+
+ private lateinit var setDefaultPropertyValueUseCase: SetDefaultPropertyValueUseCase
+
+ @Before
+ fun setUp() {
+ init(this)
+
+ setDefaultPropertyValueUseCase =
+ SetDefaultPropertyValueUseCase(defaultPropertyValueRepository)
+ }
+
+ @Test
+ fun `should set new property value`() = runTest {
+ coEvery {
+ defaultPropertyValueRepository.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ } returns Right(Unit)
+
+ // when setting new property value
+ val result = setDefaultPropertyValueUseCase.run(
+ SetDefaultPropertyValueUseCase.Params(
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ propertyValue = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ )
+
+ // then
+ assertTrue(result.isRight)
+ }
+
+ @Test
+ fun `should return PropertyValueFailure if something goes wrong`() = runTest {
+ coEvery {
+ defaultPropertyValueRepository.setPropertyValue(
+ Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ } returns Left(PropertyValueFailure("STATUT_BIO"))
+
+ // when setting new property value
+ val result = setDefaultPropertyValueUseCase.run(
+ SetDefaultPropertyValueUseCase.Params(
+ taxonomy = Taxonomy(
+ kingdom = "Animalia",
+ group = "Oiseaux"
+ ),
+ propertyValue = PropertyValue(
+ code = "STATUT_BIO",
+ label = "Hibernation",
+ value = 33L
+ )
+ )
+ )
+
+ // then
+ assertTrue(result.isLeft)
+ assertEquals(
+ result.fold(::identity) {},
+ PropertyValueFailure("STATUT_BIO")
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/input/PropertyValueTest.kt b/occtax/src/test/java/fr/geonature/occtax/input/PropertyValueTest.kt
index 313f7afb..d6731fbd 100644
--- a/occtax/src/test/java/fr/geonature/occtax/input/PropertyValueTest.kt
+++ b/occtax/src/test/java/fr/geonature/occtax/input/PropertyValueTest.kt
@@ -19,6 +19,22 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class PropertyValueTest {
+ @Test
+ fun `should be the same property value`() {
+ assertEquals(
+ PropertyValue(
+ "STATUT_BIO",
+ "Non renseigné",
+ 29
+ ),
+ PropertyValue(
+ "STATUT_BIO",
+ "Non renseigné",
+ 29
+ )
+ )
+ }
+
@Test
fun testCreateFromNomenclature() {
assertEquals(
diff --git a/occtax/src/test/java/fr/geonature/occtax/settings/NomenclatureSettingsTest.kt b/occtax/src/test/java/fr/geonature/occtax/settings/NomenclatureSettingsTest.kt
new file mode 100644
index 00000000..411a3138
--- /dev/null
+++ b/occtax/src/test/java/fr/geonature/occtax/settings/NomenclatureSettingsTest.kt
@@ -0,0 +1,94 @@
+package fr.geonature.occtax.settings
+
+import android.os.Parcel
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Unit tests about [NomenclatureSettings].
+ *
+ * @author S. Grimault
+ */
+@RunWith(RobolectricTestRunner::class)
+class NomenclatureSettingsTest {
+
+ @Test
+ fun `should create NomenclatureSettings from Parcelable`() {
+ // given a NomenclatureSettings instance
+ val nomenclatureSettings = NomenclatureSettings(
+ saveDefaultValues = true,
+ information = listOf(
+ PropertySettings(
+ "METH_OBS",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ "ETA_BIO",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ "METH_DETERMIN",
+ visible = true,
+ default = false
+ ),
+ PropertySettings(
+ "STATUT_BIO",
+ visible = true,
+ default = false
+ ),
+ PropertySettings(
+ "NATURALITE",
+ visible = true,
+ default = false
+ ),
+ PropertySettings(
+ "PREUVE_EXIST",
+ visible = true,
+ default = false
+ )
+ ),
+ counting = listOf(
+ PropertySettings(
+ "STADE_VIE",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ "SEXE",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ "OBJ_DENBR",
+ visible = true,
+ default = true
+ ),
+ PropertySettings(
+ "TYP_DENBR",
+ visible = true,
+ default = true
+ )
+ )
+ )
+
+ // when we obtain a Parcel object to write NomenclatureSettings instance to it
+ val parcel = Parcel.obtain()
+ nomenclatureSettings.writeToParcel(
+ parcel,
+ 0
+ )
+
+ // reset the parcel for reading
+ parcel.setDataPosition(0)
+
+ // then
+ assertEquals(
+ nomenclatureSettings,
+ NomenclatureSettings.CREATOR.createFromParcel(parcel)
+ )
+ }
+}
\ No newline at end of file
diff --git a/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt
index 663b9e6d..3211138d 100644
--- a/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt
+++ b/occtax/src/test/java/fr/geonature/occtax/settings/io/AppSettingsJsonReaderTest.kt
@@ -79,7 +79,8 @@ class AppSettingsJsonReaderTest {
),
inputSettings = InputSettings(dateSettings = InputDateSettings.DEFAULT),
nomenclatureSettings = NomenclatureSettings(
- arrayListOf(
+ saveDefaultValues = true,
+ information = arrayListOf(
PropertySettings(
"METH_OBS",
visible = true,
@@ -111,7 +112,7 @@ class AppSettingsJsonReaderTest {
default = false
)
),
- arrayListOf(
+ counting = arrayListOf(
PropertySettings(
"STADE_VIE",
visible = true,
diff --git a/occtax/src/test/resources/fixtures/input_simple.json b/occtax/src/test/resources/fixtures/input_simple.json
index 2c226a48..79837781 100644
--- a/occtax/src/test/resources/fixtures/input_simple.json
+++ b/occtax/src/test/resources/fixtures/input_simple.json
@@ -30,8 +30,11 @@
"regne": "Animalia",
"group2_inpn": "Ascidies",
"properties": {
- "meth_obs": {
- "value": 41
+ "comment": {
+ "value": "Some comment"
+ },
+ "determiner": {
+ "value": "Determiner value"
},
"eta_bio": {
"value": 29
@@ -39,23 +42,20 @@
"meth_determin": {
"value": 445
},
- "determiner": {
- "value": "Determiner value"
+ "meth_obs": {
+ "value": 41
},
- "statut_bio": {
- "value": 29
+ "naturalite": {
+ "value": 160
},
"occ_comportement": {
"value": 580
},
- "naturalite": {
- "value": 160
- },
"preuve_exist": {
"value": 81
},
- "comment": {
- "value": "Some comment"
+ "statut_bio": {
+ "value": 29
},
"counting": [
{
@@ -77,15 +77,15 @@
}
]
},
- "id_nomenclature_obs_technique": 41,
+ "comment": "Some comment",
+ "determiner": "Determiner value",
"id_nomenclature_bio_condition": 29,
"id_nomenclature_determination_method": 445,
- "determiner": "Determiner value",
- "id_nomenclature_bio_status": 29,
- "id_nomenclature_behaviour": 580,
+ "id_nomenclature_obs_technique": 41,
"id_nomenclature_naturalness": 160,
+ "id_nomenclature_behaviour": 580,
"id_nomenclature_exist_proof": 81,
- "comment": "Some comment",
+ "id_nomenclature_bio_status": 29,
"cor_counting_occtax": [
{
"id_nomenclature_life_stage": 2,
diff --git a/occtax/src/test/resources/fixtures/settings_occtax.json b/occtax/src/test/resources/fixtures/settings_occtax.json
index 1ac7ac45..8c59023c 100644
--- a/occtax/src/test/resources/fixtures/settings_occtax.json
+++ b/occtax/src/test/resources/fixtures/settings_occtax.json
@@ -29,6 +29,7 @@
]
},
"nomenclature": {
+ "save_default_values": true,
"information": [
"METH_OBS",
{