From 74122fee41465f9d727baf623d9473645e1cd831 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 10:12:20 +0100 Subject: [PATCH 001/222] [ANDROSDK-1695] Add TrackerDataView to RelationshipConstraint --- .../IgnoreTrackerDataViewColumnAdapter.kt | 33 ++++++++ .../relationship/RelationshipConstraint.java | 8 ++ .../core/relationship/TrackerDataView.java | 77 +++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt new file mode 100644 index 0000000000..45942c9181 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal + +import org.hisp.dhis.android.core.relationship.TrackerDataView + +internal class IgnoreTrackerDataViewColumnAdapter : + IgnoreColumnAdapter() diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java index e951cdd8f3..d1e6c9ad60 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java @@ -41,6 +41,8 @@ import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipConstraintTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipEntityTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreRelationshipListColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreTrackerDataViewColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.ObjectWithUid; @@ -74,6 +76,10 @@ public abstract class RelationshipConstraint extends BaseObject { @ColumnAdapter(ObjectWithUidColumnAdapter.class) public abstract ObjectWithUid programStage(); + @Nullable + @ColumnAdapter(IgnoreTrackerDataViewColumnAdapter.class) + public abstract TrackerDataView trackerDataView(); + public static RelationshipConstraint create(Cursor cursor) { return AutoValue_RelationshipConstraint.createFromCursor(cursor); } @@ -100,6 +106,8 @@ public static abstract class Builder extends BaseObject.Builder { public abstract Builder programStage(ObjectWithUid programStage); + public abstract Builder trackerDataView(TrackerDataView trackerDataView); + public abstract RelationshipConstraint build(); } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java new file mode 100644 index 0000000000..60b81aeb22 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.relationship; + +import android.database.Cursor; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; +import org.hisp.dhis.android.core.common.BaseObject; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = AutoValue_TrackerDataView.Builder.class) +public abstract class TrackerDataView extends BaseObject { + + @Nullable + @ColumnAdapter(StringListColumnAdapter.class) + public abstract List attributes(); + + @Nullable + @ColumnAdapter(StringListColumnAdapter.class) + public abstract List dataElements(); + + public static TrackerDataView create(Cursor cursor) { + return AutoValue_TrackerDataView.createFromCursor(cursor); + } + + public static Builder builder() { + return new AutoValue_TrackerDataView.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public static abstract class Builder extends BaseObject.Builder { + + public abstract Builder attributes(List attributes); + + public abstract Builder dataElements(List dataElements); + + public abstract TrackerDataView build(); + } +} \ No newline at end of file From bdea97024e936041f13d88f1c7ac57ce3d6ab6d2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 10:12:37 +0100 Subject: [PATCH 002/222] [ANDROSDK-1695] Add TrackerDataView to RelationshipConstraint table info --- .../core/relationship/RelationshipConstraintTableInfo.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraintTableInfo.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraintTableInfo.java index 31e17f4aac..9133e00bd2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraintTableInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraintTableInfo.java @@ -57,6 +57,8 @@ public static class Columns extends CoreColumns { public static final String TRACKED_ENTITY_TYPE = "trackedEntityType"; public static final String PROGRAM = "program"; public static final String PROGRAM_STAGE = "programStage"; + public static final String TRACKER_DATA_VIEW_ATTRIBUTES = "trackerDataViewAttributes"; + public static final String TRACKER_DATA_VIEW_DATA_ELEMENTS = "trackerDataViewDataElements"; @Override public String[] all() { @@ -66,7 +68,9 @@ public String[] all() { RELATIONSHIP_ENTITY, TRACKED_ENTITY_TYPE, PROGRAM, - PROGRAM_STAGE + PROGRAM_STAGE, + TRACKER_DATA_VIEW_ATTRIBUTES, + TRACKER_DATA_VIEW_DATA_ELEMENTS ); } From df4979fc6418ca836843aa7ce1af79524babb66e Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 10:12:45 +0100 Subject: [PATCH 003/222] [ANDROSDK-1695] Add TrackerDataView to RelationshipConstraint store --- .../internal/RelationshipConstraintStoreImpl.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreImpl.kt index 19b9b54e2f..c580e2ff37 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreImpl.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.relationship.internal import android.database.Cursor import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper import org.hisp.dhis.android.core.arch.db.stores.binders.internal.WhereStatementBinder @@ -59,10 +60,12 @@ internal class RelationshipConstraintStoreImpl( w.bind(4, getUidOrNull(o.trackedEntityType())) w.bind(5, getUidOrNull(o.program())) w.bind(6, getUidOrNull(o.programStage())) + w.bind(7, StringListColumnAdapter.serialize(o.trackerDataView()?.attributes())) + w.bind(8, StringListColumnAdapter.serialize(o.trackerDataView()?.dataElements())) } private val WHERE_UPDATE_BINDER = WhereStatementBinder { o: RelationshipConstraint, w: StatementWrapper -> - w.bind(7, getUidOrNull(o.relationshipType())) - w.bind(8, o.constraintType()) + w.bind(9, getUidOrNull(o.relationshipType())) + w.bind(10, o.constraintType()) } private val WHERE_DELETE_BINDER = WhereStatementBinder { o: RelationshipConstraint, w: StatementWrapper -> w.bind(1, getUidOrNull(o.relationshipType())) From 6b6f1767400d0b10f6bdce1e5c74367139ced6f6 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:54:32 +0100 Subject: [PATCH 004/222] [ANDROSDK-1695] Add TrackerDataView to snapshot and migrations --- core/src/main/assets/migrations/156.sql | 4 ++++ core/src/main/assets/snapshots/snapshot.sql | 2 +- .../core/arch/db/access/internal/BaseDatabaseOpenHelper.kt | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 core/src/main/assets/migrations/156.sql diff --git a/core/src/main/assets/migrations/156.sql b/core/src/main/assets/migrations/156.sql new file mode 100644 index 0000000000..7c3d52a33e --- /dev/null +++ b/core/src/main/assets/migrations/156.sql @@ -0,0 +1,4 @@ +# Add trackerDataView to RelationshipConstraint (ANDROSDK-1695) + +ALTER TABLE RelationshipConstraint ADD COLUMN trackerDataViewAttributes TEXT; +ALTER TABLE RelationshipConstraint ADD COLUMN trackerDataViewDataElements TEXT; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 291cbaca5e..810ae6dc45 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -40,7 +40,7 @@ CREATE TABLE Section (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL U CREATE TABLE SectionDataElementLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, section TEXT NOT NULL, dataElement TEXT NOT NULL, sortOrder INTEGER, FOREIGN KEY (section) REFERENCES Section (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (dataElement) REFERENCES DataElement (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (section, dataElement)); CREATE TABLE DataSetCompulsoryDataElementOperandsLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, dataSet TEXT NOT NULL, dataElementOperand TEXT NOT NULL, FOREIGN KEY (dataSet) REFERENCES DataSet (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (dataElementOperand) REFERENCES DataElementOperand (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (dataSet, dataElementOperand)); CREATE TABLE DataInputPeriod (_id INTEGER PRIMARY KEY AUTOINCREMENT, dataSet TEXT NOT NULL, period TEXT NOT NULL, openingDate TEXT, closingDate TEXT, FOREIGN KEY (dataSet) REFERENCES DataSet (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE RelationshipConstraint (_id INTEGER PRIMARY KEY AUTOINCREMENT, relationshipType TEXT NOT NULL, constraintType TEXT NOT NULL, relationshipEntity TEXT, trackedEntityType TEXT, program TEXT, programStage TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (relationshipType, constraintType)); +CREATE TABLE RelationshipConstraint (_id INTEGER PRIMARY KEY AUTOINCREMENT, relationshipType TEXT NOT NULL, constraintType TEXT NOT NULL, relationshipEntity TEXT, trackedEntityType TEXT, program TEXT, programStage TEXT, trackerDataViewAttributes TEXT, trackerDataViewDataElements TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (relationshipType, constraintType)); CREATE TABLE RelationshipItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, relationship TEXT NOT NULL, relationshipItemType TEXT NOT NULL, trackedEntityInstance TEXT, enrollment TEXT, event TEXT, FOREIGN KEY (relationship) REFERENCES Relationship (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED FOREIGN KEY (trackedEntityInstance) REFERENCES TrackedEntityInstance (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (enrollment) REFERENCES Enrollment (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (event) REFERENCES Event (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE OrganisationUnitGroup (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, shortName TEXT, displayShortName TEXT); CREATE TABLE OrganisationUnitOrganisationUnitGroupLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, organisationUnit TEXT NOT NULL, organisationUnitGroup TEXT NOT NULL, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (organisationUnitGroup) REFERENCES OrganisationUnitGroup (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (organisationUnit, organisationUnitGroup)); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index 7d8aadd20a..c503e20202 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 155 + const val VERSION = 156 } } From 2a8983eae0696c591152ed7c166753811f3a0bf2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:55:07 +0100 Subject: [PATCH 005/222] [ANDROSDK-1695] Add TrackerDataView fields --- .../core/relationship/TrackerDataView.java | 26 ++++++++++- .../RelationshipConstraintFields.java | 7 ++- .../internal/TrackerDataViewFields.kt | 45 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java index 60b81aeb22..c3bf00317a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java @@ -40,6 +40,7 @@ import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; +import java.util.Collections; import java.util.List; @AutoValue @@ -72,6 +73,29 @@ public static abstract class Builder extends BaseObject.Builder { public abstract Builder dataElements(List dataElements); - public abstract TrackerDataView build(); + abstract TrackerDataView autoBuild(); + + //Auxiliary fields + abstract List attributes(); + + abstract List dataElements(); + + public TrackerDataView build() { + + try { + attributes(); + } catch (IllegalStateException e) { + attributes(Collections.emptyList()); + } + + try { + dataElements(); + } catch (IllegalStateException e) { + dataElements(Collections.emptyList()); + } + + return autoBuild(); + } + } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java index 5a3dc80e01..66d1c2dd37 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java @@ -30,11 +30,14 @@ import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; +import org.hisp.dhis.android.core.dataset.internal.SectionFields; import org.hisp.dhis.android.core.relationship.RelationshipConstraint; import org.hisp.dhis.android.core.relationship.RelationshipConstraintTableInfo.Columns; import org.hisp.dhis.android.core.relationship.RelationshipEntityType; +import org.hisp.dhis.android.core.relationship.TrackerDataView; final class RelationshipConstraintFields { + private static final String TRACKER_DATA_VIEW = "trackerDataView"; private static final FieldsHelper fh = new FieldsHelper<>(); @@ -44,7 +47,9 @@ final class RelationshipConstraintFields { fh.field(Columns.RELATIONSHIP_ENTITY), fh.nestedFieldWithUid(Columns.TRACKED_ENTITY_TYPE), fh.nestedFieldWithUid(Columns.PROGRAM), - fh.nestedFieldWithUid(Columns.PROGRAM_STAGE) + fh.nestedFieldWithUid(Columns.PROGRAM_STAGE), + fh.nestedField(TRACKER_DATA_VIEW) + .with(TrackerDataViewFields.INSTANCE.getAllFields()) ).build(); private RelationshipConstraintFields() { diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt new file mode 100644 index 0000000000..cc6904c0c3 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.relationship.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.relationship.TrackerDataView + +internal object TrackerDataViewFields { + private const val ATTRIBUTES = "attributes" + private const val DATA_ELEMENTS = "dataElements" + + private val fh = FieldsHelper() + + val allFields: Fields = Fields.builder() + .fields( + fh.field(ATTRIBUTES), + fh.field(DATA_ELEMENTS) + ).build() +} From 9e5745026bc6d7e0dea0936f18fe76e05f67ca02 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:55:50 +0100 Subject: [PATCH 006/222] [ANDROSDK-1695] Add TrackerDataView to json samples --- .../relationship/relationship_type_30.json | 17 +++++++++++++++++ .../relationship/relationship_type_32.json | 17 +++++++++++++++++ .../relationship/RelationshipType30Should.java | 7 +++++++ .../relationship/RelationshipType32Should.java | 7 +++++++ 4 files changed, 48 insertions(+) diff --git a/core/src/sharedTest/resources/relationship/relationship_type_30.json b/core/src/sharedTest/resources/relationship/relationship_type_30.json index 1b087dd8d1..1c2d84650f 100644 --- a/core/src/sharedTest/resources/relationship/relationship_type_30.json +++ b/core/src/sharedTest/resources/relationship/relationship_type_30.json @@ -20,12 +20,29 @@ "relationshipEntity": "TRACKED_ENTITY_INSTANCE", "trackedEntityType": { "id": "nEenWmSyUEp" + }, + "trackerDataView": { + "attributes": [ + "b0vcadVrn08", + "qXS2NDUEAOS" + ], + "dataElements": [ + "ciWE5jde1ax", + "hB9F8vKFmlk", + "uFAQYm3UgBL" + ] } }, "toConstraint": { "relationshipEntity": "PROGRAM_INSTANCE", "program": { "id": "WSGAb5XwJ3Y" + }, + "trackerDataView": { + "attributes": [ + "b0vcadVrn08" + ], + "dataElements": [] } } } \ No newline at end of file diff --git a/core/src/sharedTest/resources/relationship/relationship_type_32.json b/core/src/sharedTest/resources/relationship/relationship_type_32.json index 88254731a5..aa324920ee 100644 --- a/core/src/sharedTest/resources/relationship/relationship_type_32.json +++ b/core/src/sharedTest/resources/relationship/relationship_type_32.json @@ -24,12 +24,29 @@ "relationshipEntity": "PROGRAM_INSTANCE", "program": { "id": "WSGAb5XwJ3Y" + }, + "trackerDataView": { + "attributes": [ + "b0vcadVrn08", + "qXS2NDUEAOS" + ], + "dataElements": [ + "ciWE5jde1ax", + "hB9F8vKFmlk", + "uFAQYm3UgBL" + ] } }, "fromConstraint": { "relationshipEntity": "TRACKED_ENTITY_INSTANCE", "trackedEntityType": { "id": "nEenWmSyUEp" + }, + "trackerDataView": { + "attributes": [ + "b0vcadVrn08" + ], + "dataElements": [] } } } \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java index 72a1ed3e49..34648209e9 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java +++ b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java @@ -63,9 +63,16 @@ public void map_from_json_string() throws IOException, ParseException { assertThat(relationshipType.fromConstraint()).isNotNull(); assertThat(relationshipType.fromConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.TRACKED_ENTITY_INSTANCE); assertThat(relationshipType.fromConstraint().trackedEntityType().uid()).isEqualTo("nEenWmSyUEp"); + assertThat(relationshipType.fromConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); + assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().isEmpty()).isTrue(); assertThat(relationshipType.toConstraint()).isNotNull(); assertThat(relationshipType.toConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.PROGRAM_INSTANCE); assertThat(relationshipType.toConstraint().program().uid()).isEqualTo("WSGAb5XwJ3Y"); + assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); + assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(1)).isEqualTo("qXS2NDUEAOS"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(0)).isEqualTo("ciWE5jde1ax"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(1)).isEqualTo("hB9F8vKFmlk"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(2)).isEqualTo("uFAQYm3UgBL"); assertThat(relationshipType.bidirectional()).isFalse(); assertThat(relationshipType.access().data().read()).isTrue(); assertThat(relationshipType.access().data().write()).isFalse(); diff --git a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType32Should.java b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType32Should.java index 3acae5177f..40a8df725a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType32Should.java +++ b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType32Should.java @@ -63,9 +63,16 @@ public void map_from_json_string() throws IOException, ParseException { assertThat(relationshipType.fromConstraint()).isNotNull(); assertThat(relationshipType.fromConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.TRACKED_ENTITY_INSTANCE); assertThat(relationshipType.fromConstraint().trackedEntityType().uid()).isEqualTo("nEenWmSyUEp"); + assertThat(relationshipType.fromConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); + assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().isEmpty()).isTrue(); assertThat(relationshipType.toConstraint()).isNotNull(); assertThat(relationshipType.toConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.PROGRAM_INSTANCE); assertThat(relationshipType.toConstraint().program().uid()).isEqualTo("WSGAb5XwJ3Y"); + assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); + assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(1)).isEqualTo("qXS2NDUEAOS"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(0)).isEqualTo("ciWE5jde1ax"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(1)).isEqualTo("hB9F8vKFmlk"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(2)).isEqualTo("uFAQYm3UgBL"); assertThat(relationshipType.bidirectional()).isTrue(); assertThat(relationshipType.access().data().read()).isTrue(); assertThat(relationshipType.access().data().write()).isFalse(); From f1e7d887eadeead01addf65683bbeec2006ce1a3 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:56:12 +0100 Subject: [PATCH 007/222] [ANDROSDK-1695] Rename .java to .kt --- ...hould.java => RelationshipConstraintStoreIntegrationShould.kt} | 0 ...hipConstraintSamples.java => RelationshipConstraintSamples.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/{RelationshipConstraintStoreIntegrationShould.java => RelationshipConstraintStoreIntegrationShould.kt} (100%) rename core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/{RelationshipConstraintSamples.java => RelationshipConstraintSamples.kt} (100%) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt similarity index 100% rename from core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.java rename to core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt similarity index 100% rename from core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.java rename to core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt From 3c1b3e7fa6b29255c3bed15086167703b0aff21d Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:56:12 +0100 Subject: [PATCH 008/222] [ANDROSDK-1695] Add TrackerDataView to RelationshipConstraintStoreIntegrationShould --- ...ionshipConstraintStoreIntegrationShould.kt | 48 ++++++++----------- .../RelationshipConstraintSamples.kt | 44 +++++++++-------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt index 7862a08607..19fc2d90c5 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt @@ -25,36 +25,30 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.relationship.internal -package org.hisp.dhis.android.core.relationship.internal; +import org.hisp.dhis.android.core.data.database.ObjectWithoutUidStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.relationship.RelationshipConstraintSamples.relationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipConstraintTableInfo +import org.hisp.dhis.android.core.relationship.RelationshipEntityType +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.runner.RunWith -import org.hisp.dhis.android.core.data.database.ObjectWithoutUidStoreAbstractIntegrationShould; -import org.hisp.dhis.android.core.data.relationship.RelationshipConstraintSamples; -import org.hisp.dhis.android.core.relationship.RelationshipConstraint; -import org.hisp.dhis.android.core.relationship.RelationshipConstraintTableInfo; -import org.hisp.dhis.android.core.relationship.RelationshipEntityType; -import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory; -import org.hisp.dhis.android.core.utils.runner.D2JunitRunner; -import org.junit.runner.RunWith; - -@RunWith(D2JunitRunner.class) -public class RelationshipConstraintStoreIntegrationShould extends - ObjectWithoutUidStoreAbstractIntegrationShould { - - public RelationshipConstraintStoreIntegrationShould() { - super(new RelationshipConstraintStoreImpl(TestDatabaseAdapterFactory.get()), - RelationshipConstraintTableInfo.TABLE_INFO, TestDatabaseAdapterFactory.get()); - } - - @Override - protected RelationshipConstraint buildObject() { - return RelationshipConstraintSamples.getRelationshipConstraint(); +@RunWith(D2JunitRunner::class) +class RelationshipConstraintStoreIntegrationShould : + ObjectWithoutUidStoreAbstractIntegrationShould( + RelationshipConstraintStoreImpl(TestDatabaseAdapterFactory.get()), + RelationshipConstraintTableInfo.TABLE_INFO, TestDatabaseAdapterFactory.get() + ) { + override fun buildObject(): RelationshipConstraint { + return relationshipConstraint } - @Override - protected RelationshipConstraint buildObjectToUpdate() { - return RelationshipConstraintSamples.getRelationshipConstraint().toBuilder() - .relationshipEntity(RelationshipEntityType.PROGRAM_INSTANCE) - .build(); + override fun buildObjectToUpdate(): RelationshipConstraint { + return relationshipConstraint.toBuilder() + .relationshipEntity(RelationshipEntityType.PROGRAM_INSTANCE) + .build() } } \ No newline at end of file diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt index 8331c9304e..bce92c3b78 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt @@ -25,26 +25,30 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.data.relationship -package org.hisp.dhis.android.core.data.relationship; +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.relationship.RelationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipEntityType +import org.hisp.dhis.android.core.relationship.TrackerDataView -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.relationship.RelationshipConstraint; -import org.hisp.dhis.android.core.relationship.RelationshipConstraintType; -import org.hisp.dhis.android.core.relationship.RelationshipEntityType; - - -public class RelationshipConstraintSamples { - - public static RelationshipConstraint getRelationshipConstraint() { - return RelationshipConstraint.builder() - .id(1L) - .relationshipType(ObjectWithUid.create("relationship_type_uid")) - .constraintType(RelationshipConstraintType.FROM) - .relationshipEntity(RelationshipEntityType.TRACKED_ENTITY_INSTANCE) - .trackedEntityType(ObjectWithUid.create("tracked_entity_type_uid")) - .program(ObjectWithUid.create("program_uid")) - .programStage(ObjectWithUid.create("program_stage_uid")) - .build(); - } +object RelationshipConstraintSamples { + @JvmStatic + val relationshipConstraint: RelationshipConstraint + get() = RelationshipConstraint.builder() + .id(1L) + .relationshipType(ObjectWithUid.create("relationship_type_uid")) + .constraintType(RelationshipConstraintType.FROM) + .relationshipEntity(RelationshipEntityType.TRACKED_ENTITY_INSTANCE) + .trackedEntityType(ObjectWithUid.create("tracked_entity_type_uid")) + .program(ObjectWithUid.create("program_uid")) + .programStage(ObjectWithUid.create("program_stage_uid")) + .trackerDataView( + TrackerDataView.builder() + .attributes(listOf("attribute_uid_1", "attribute_uid_3", "attribute_uid_3")) + .dataElements(listOf("data_element_uid_1", "data_element_uid_2", "data_element_uid_3")) + .build() + ) + .build() } \ No newline at end of file From f342e08ed67c4dade1704e125fe1dba470b63391 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 11:56:53 +0100 Subject: [PATCH 009/222] [ANDROSDK-1695] Remove unused import --- .../dhis/android/core/relationship/RelationshipConstraint.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java index d1e6c9ad60..8481c48355 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java @@ -41,7 +41,6 @@ import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipConstraintTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipEntityTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; -import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreRelationshipListColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreTrackerDataViewColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.ObjectWithUid; From 54d72945216093b9f1f356183287e8b049488786 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 12:05:32 +0100 Subject: [PATCH 010/222] [ANDROSDK-1695] ktlint format --- .../internal/RelationshipConstraintStoreIntegrationShould.kt | 5 +++-- .../core/relationship/internal/TrackerDataViewFields.kt | 2 +- .../core/data/relationship/RelationshipConstraintSamples.kt | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt index 19fc2d90c5..6dcbaab026 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintStoreIntegrationShould.kt @@ -40,7 +40,8 @@ import org.junit.runner.RunWith class RelationshipConstraintStoreIntegrationShould : ObjectWithoutUidStoreAbstractIntegrationShould( RelationshipConstraintStoreImpl(TestDatabaseAdapterFactory.get()), - RelationshipConstraintTableInfo.TABLE_INFO, TestDatabaseAdapterFactory.get() + RelationshipConstraintTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), ) { override fun buildObject(): RelationshipConstraint { return relationshipConstraint @@ -51,4 +52,4 @@ class RelationshipConstraintStoreIntegrationShould : .relationshipEntity(RelationshipEntityType.PROGRAM_INSTANCE) .build() } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt index cc6904c0c3..2b52e30393 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/TrackerDataViewFields.kt @@ -40,6 +40,6 @@ internal object TrackerDataViewFields { val allFields: Fields = Fields.builder() .fields( fh.field(ATTRIBUTES), - fh.field(DATA_ELEMENTS) + fh.field(DATA_ELEMENTS), ).build() } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt index bce92c3b78..b4b31153db 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipConstraintSamples.kt @@ -48,7 +48,7 @@ object RelationshipConstraintSamples { TrackerDataView.builder() .attributes(listOf("attribute_uid_1", "attribute_uid_3", "attribute_uid_3")) .dataElements(listOf("data_element_uid_1", "data_element_uid_2", "data_element_uid_3")) - .build() + .build(), ) .build() -} \ No newline at end of file +} From 7a138fa10e06409b1a2a3138bb7221db25820d67 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 8 Jan 2024 12:41:34 +0100 Subject: [PATCH 011/222] [ANDROSDK-1695] Add relationship store tests --- ...llectionRepositoryImplIntegrationShould.kt | 32 +++++++++++++++++++ .../relationship/RelationshipTypeSamples.kt | 26 +++++++++++++++ .../RelationshipType30Should.java | 10 +++--- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyIdentifiableCollectionRepositoryImplIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyIdentifiableCollectionRepositoryImplIntegrationShould.kt index 12c2fc1f05..58a3fbcb34 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyIdentifiableCollectionRepositoryImplIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyIdentifiableCollectionRepositoryImplIntegrationShould.kt @@ -83,6 +83,38 @@ class ReadOnlyIdentifiableCollectionRepositoryImplIntegrationShould : BaseMockIn .isEqualTo(RelationshipTypeSamples.TET_FOR_RELATIONSHIP_3_UID) } + @Test + fun get_tracker_data_view_from_relationship_constraint() { + val relationshipType = relationshipTypeCollectionRepository + .byConstraint( + RelationshipEntityType.TRACKED_ENTITY_INSTANCE, + RelationshipTypeSamples.TET_FOR_RELATIONSHIP_3_UID, + RelationshipConstraintType.FROM, + ) + .withConstraints() + .blockingGet() + + val fromTrackerDataView = relationshipType[0].fromConstraint()?.trackerDataView() + val toTrackerDataView = relationshipType[0].toConstraint()?.trackerDataView() + + assertThat(fromTrackerDataView?.attributes()?.get(0)) + .isEqualTo(RelationshipTypeSamples.ATTRIBUTE_1) + assertThat(fromTrackerDataView?.attributes()?.get(1)) + .isEqualTo(RelationshipTypeSamples.ATTRIBUTE_2) + assertThat(fromTrackerDataView?.attributes()?.get(2)) + .isEqualTo(RelationshipTypeSamples.ATTRIBUTE_3) + + assertThat(fromTrackerDataView?.dataElements()?.get(0)) + .isEqualTo(RelationshipTypeSamples.DATA_ELEMENT_1) + assertThat(fromTrackerDataView?.dataElements()?.get(1)) + .isEqualTo(RelationshipTypeSamples.DATA_ELEMENT_2) + + assertThat(toTrackerDataView?.attributes()?.get(0)) + .isEqualTo(RelationshipTypeSamples.ATTRIBUTE_3) + + assertThat(toTrackerDataView?.dataElements()?.isEmpty()).isTrue() + } + @Test fun get_relationship_2_from_object_repository_without_children() { val relationshipType = diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipTypeSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipTypeSamples.kt index f2c77705c7..23318cc93f 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipTypeSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/relationship/RelationshipTypeSamples.kt @@ -32,35 +32,59 @@ import org.hisp.dhis.android.core.relationship.RelationshipConstraint import org.hisp.dhis.android.core.relationship.RelationshipConstraintType import org.hisp.dhis.android.core.relationship.RelationshipEntityType import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.android.core.relationship.TrackerDataView object RelationshipTypeSamples { var RELATIONSHIP_TYPE_UID_1 = "RELATIONSHIP_TYPE_UID_1" var RELATIONSHIP_TYPE_UID_2 = "RELATIONSHIP_TYPE_UID_2" var RELATIONSHIP_TYPE_UID_3 = "RELATIONSHIP_TYPE_UID_3" var TET_FOR_RELATIONSHIP_3_UID = "nEenWmSyUEp" + + var ATTRIBUTE_1 = "ATTRIBUTE_1" + var ATTRIBUTE_2 = "ATTRIBUTE_2" + var ATTRIBUTE_3 = "ATTRIBUTE_3" + var DATA_ELEMENT_1 = "DATA_ELEMENT_1" + var DATA_ELEMENT_2 = "DATA_ELEMENT_2" + + var TRACKER_DATA_VIEW_1 = TrackerDataView + .builder() + .attributes(listOf(ATTRIBUTE_1, ATTRIBUTE_2, ATTRIBUTE_3)) + .dataElements(listOf(DATA_ELEMENT_1, DATA_ELEMENT_2)) + .build() + + var TRACKER_DATA_VIEW_2 = TrackerDataView + .builder() + .attributes(listOf(ATTRIBUTE_3)) + .dataElements(emptyList()) + .build() + var FROM_CONSTRAINT_1 = RelationshipConstraint .builder() .id(100L) .constraintType(RelationshipConstraintType.FROM) .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_1)) + .trackerDataView(TRACKER_DATA_VIEW_1) .build() var TO_CONSTRAINT_1 = RelationshipConstraint .builder() .id(200L) .constraintType(RelationshipConstraintType.TO) .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_1)) + .trackerDataView(TRACKER_DATA_VIEW_2) .build() var FROM_CONSTRAINT_2 = RelationshipConstraint .builder() .id(300L) .constraintType(RelationshipConstraintType.FROM) .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_2)) + .trackerDataView(TRACKER_DATA_VIEW_2) .build() var TO_CONSTRAINT_2 = RelationshipConstraint .builder() .id(400L) .constraintType(RelationshipConstraintType.TO) .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_2)) + .trackerDataView(TRACKER_DATA_VIEW_1) .build() var FROM_CONSTRAINT_3 = RelationshipConstraint .builder() @@ -69,12 +93,14 @@ object RelationshipTypeSamples { .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_3)) .relationshipEntity(RelationshipEntityType.TRACKED_ENTITY_INSTANCE) .trackedEntityType(ObjectWithUid.create(TET_FOR_RELATIONSHIP_3_UID)) + .trackerDataView(TRACKER_DATA_VIEW_1) .build() var TO_CONSTRAINT_3 = RelationshipConstraint .builder() .id(600L) .constraintType(RelationshipConstraintType.TO) .relationshipType(ObjectWithUid.create(RELATIONSHIP_TYPE_UID_3)) + .trackerDataView(TRACKER_DATA_VIEW_2) .build() var RELATIONSHIP_TYPE_1 = RelationshipType .builder() diff --git a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java index 34648209e9..b514b84782 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java +++ b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipType30Should.java @@ -64,15 +64,15 @@ public void map_from_json_string() throws IOException, ParseException { assertThat(relationshipType.fromConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.TRACKED_ENTITY_INSTANCE); assertThat(relationshipType.fromConstraint().trackedEntityType().uid()).isEqualTo("nEenWmSyUEp"); assertThat(relationshipType.fromConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); - assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().isEmpty()).isTrue(); + assertThat(relationshipType.fromConstraint().trackerDataView().attributes().get(1)).isEqualTo("qXS2NDUEAOS"); + assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().get(0)).isEqualTo("ciWE5jde1ax"); + assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().get(1)).isEqualTo("hB9F8vKFmlk"); + assertThat(relationshipType.fromConstraint().trackerDataView().dataElements().get(2)).isEqualTo("uFAQYm3UgBL"); assertThat(relationshipType.toConstraint()).isNotNull(); assertThat(relationshipType.toConstraint().relationshipEntity()).isEqualTo(RelationshipEntityType.PROGRAM_INSTANCE); assertThat(relationshipType.toConstraint().program().uid()).isEqualTo("WSGAb5XwJ3Y"); assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(0)).isEqualTo("b0vcadVrn08"); - assertThat(relationshipType.toConstraint().trackerDataView().attributes().get(1)).isEqualTo("qXS2NDUEAOS"); - assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(0)).isEqualTo("ciWE5jde1ax"); - assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(1)).isEqualTo("hB9F8vKFmlk"); - assertThat(relationshipType.toConstraint().trackerDataView().dataElements().get(2)).isEqualTo("uFAQYm3UgBL"); + assertThat(relationshipType.toConstraint().trackerDataView().dataElements().isEmpty()).isTrue(); assertThat(relationshipType.bidirectional()).isFalse(); assertThat(relationshipType.access().data().read()).isTrue(); assertThat(relationshipType.access().data().write()).isFalse(); From 3e79b3cf31c462f9244c099adc1c5a1fc252f580 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 9 Jan 2024 10:43:31 +0100 Subject: [PATCH 012/222] [BUMP-1.10.0] Bump to 1.10.0-SNAPSHOT; enable v41 --- core/gradle.properties | 4 ++-- .../java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/gradle.properties b/core/gradle.properties index 65bbaa9791..212f0f2af5 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -29,8 +29,8 @@ # Properties which are consumed by plugins/gradle-mvn-push.gradle plugin. # They are used for publishing artifact to snapshot repository. -VERSION_NAME=1.9.1-SNAPSHOT -VERSION_CODE=291 +VERSION_NAME=1.10.0-SNAPSHOT +VERSION_CODE=292 GROUP=org.hisp.dhis diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt index f885f73313..ae948dca3a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt @@ -40,7 +40,7 @@ enum class DHISVersion(internal val prefix: String, internal val supported: Bool V2_38("2.38"), V2_39("2.39"), V2_40("2.40"), - V2_41("2.41", false), + V2_41("2.41"), ; companion object { From 8a7a34196b0e56083706e06fb0839534b31659ce Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 10 Jan 2024 08:47:24 +0100 Subject: [PATCH 013/222] [BUMP-1.10.0] Adapt unit test --- .../dhis/android/core/systeminfo/DHISVersion.kt | 4 ++-- .../core/systeminfo/internal/DHISVersionShould.kt | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt index ae948dca3a..0da0d7025f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt @@ -46,7 +46,7 @@ enum class DHISVersion(internal val prefix: String, internal val supported: Bool companion object { @JvmStatic fun getValue(versionStr: String): DHISVersion? { - return values().find { versionStr.startsWith(it.prefix).and(it.supported) } + return entries.find { versionStr.startsWith(it.prefix).and(it.supported) } } @JvmStatic @@ -56,7 +56,7 @@ enum class DHISVersion(internal val prefix: String, internal val supported: Bool @JvmStatic fun allowedVersionsAsStr(): Array { - return values().filter { it.supported } + return entries.filter { it.supported } .map { it.prefix } .toTypedArray() } diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt index 7930d06d40..d6046b22f4 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt @@ -28,19 +28,22 @@ package org.hisp.dhis.android.core.systeminfo.internal -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.junit.Test class DHISVersionShould { @Test fun return_null_for_unsupported_versions() { - val supportedVersions = listOf("2.29", "2.41") - DHISVersion.values() - .filter { supportedVersions.contains(it.prefix) } + DHISVersion.entries .forEach { - Truth.assertThat(DHISVersion.getValue(it.prefix + ".0")).isNull() - Truth.assertThat(DHISVersion.getValue(it.prefix + ".9")).isNull() + if (it.supported) { + assertThat(DHISVersion.getValue(it.prefix + ".0")).isNotNull() + assertThat(DHISVersion.getValue(it.prefix + ".9")).isNotNull() + } else { + assertThat(DHISVersion.getValue(it.prefix + ".0")).isNull() + assertThat(DHISVersion.getValue(it.prefix + ".9")).isNull() + } } } } From 6ae9670e1bfcc08ea5a980e737c331ad2a5a960c Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 10 Jan 2024 09:12:34 +0100 Subject: [PATCH 014/222] [ANDROSDK-1695] Add TrackerDataViewColumnAdapter --- .../internal/TrackerDataViewColumnAdapter.kt} | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/{ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt => custom/internal/TrackerDataViewColumnAdapter.kt} (75%) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerDataViewColumnAdapter.kt similarity index 75% rename from core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt rename to core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerDataViewColumnAdapter.kt index 45942c9181..c60559545a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerDataViewColumnAdapter.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerDataViewColumnAdapter.kt @@ -25,9 +25,18 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal +package org.hisp.dhis.android.core.arch.db.adapters.custom.internal +import android.content.ContentValues +import android.database.Cursor +import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter import org.hisp.dhis.android.core.relationship.TrackerDataView +class TrackerDataViewColumnAdapter : ColumnTypeAdapter { + override fun fromCursor(cursor: Cursor, columnName: String): TrackerDataView? { + return TrackerDataView.create(cursor) + } -internal class IgnoreTrackerDataViewColumnAdapter : - IgnoreColumnAdapter() + override fun toContentValues(values: ContentValues, columnName: String, value: TrackerDataView?) { + value?.toContentValues() + } +} From 4aa2c98b9af0d6f3098d46250a6b3ef7ddd44089 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 10 Jan 2024 09:24:55 +0100 Subject: [PATCH 015/222] [ANDROSDK-1695] Add column names and fix assertTypeWithConstraints method --- .../collection/RelationshipTypeAsserts.kt | 12 ++++++++---- .../core/relationship/RelationshipConstraint.java | 4 ++-- .../android/core/relationship/TrackerDataView.java | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt index ea3c3fa493..0a3ec31418 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt @@ -52,14 +52,18 @@ internal object RelationshipTypeAsserts : BaseRealIntegrationTest() { fun assertTypeWithConstraints(target: RelationshipType, reference: RelationshipType) { val prunedTarget = target.toBuilder() .id(null) - .toConstraint(target.toConstraint()?.toBuilder()?.id(null)?.build()) - .fromConstraint(target.fromConstraint()?.toBuilder()?.id(null)?.build()) + .toConstraint(target.toConstraint()?.toBuilder()?.id(null)?. + trackerDataView(target.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) + .fromConstraint(target.fromConstraint()?.toBuilder()?.id(null)?. + trackerDataView(target.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) .build() val prunedReference = reference.toBuilder() .id(null) - .toConstraint(reference.toConstraint()?.toBuilder()?.id(null)?.build()) - .fromConstraint(reference.fromConstraint()?.toBuilder()?.id(null)?.build()) + .toConstraint(reference.toConstraint()?.toBuilder()?.id(null)?. + trackerDataView(reference.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) + .fromConstraint(reference.fromConstraint()?.toBuilder()?.id(null)?. + trackerDataView(reference.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) .build() assertThat(prunedTarget).isEqualTo(prunedReference) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java index 8481c48355..5daac24b4e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java @@ -38,10 +38,10 @@ import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerDataViewColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipConstraintTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.RelationshipEntityTypeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; -import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreTrackerDataViewColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.ObjectWithUid; @@ -76,7 +76,7 @@ public abstract class RelationshipConstraint extends BaseObject { public abstract ObjectWithUid programStage(); @Nullable - @ColumnAdapter(IgnoreTrackerDataViewColumnAdapter.class) + @ColumnAdapter(TrackerDataViewColumnAdapter.class) public abstract TrackerDataView trackerDataView(); public static RelationshipConstraint create(Cursor cursor) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java index c3bf00317a..476f108c5c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java @@ -35,6 +35,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.gabrielittner.auto.value.cursor.ColumnName; import com.google.auto.value.AutoValue; import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; @@ -48,10 +49,12 @@ public abstract class TrackerDataView extends BaseObject { @Nullable + @ColumnName(RelationshipConstraintTableInfo.Columns.TRACKER_DATA_VIEW_ATTRIBUTES) @ColumnAdapter(StringListColumnAdapter.class) public abstract List attributes(); @Nullable + @ColumnName(RelationshipConstraintTableInfo.Columns.TRACKER_DATA_VIEW_DATA_ELEMENTS) @ColumnAdapter(StringListColumnAdapter.class) public abstract List dataElements(); From 9c150cdfda9aac6599ce43df65d6c0540e9288d2 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 10 Jan 2024 10:08:23 +0100 Subject: [PATCH 016/222] [ANDROSDK-1693] Remove 'includeAllAttributes' parameter in new exporter --- .../internal/RelationshipTypeBuilder.java | 61 ------------------- .../NewTrackedEntityEndpointCallFactory.kt | 4 -- .../exporter/TrackerExporterService.kt | 4 -- 3 files changed, 69 deletions(-) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeBuilder.java diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeBuilder.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeBuilder.java deleted file mode 100644 index dd48bd6420..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.relationship.internal; - -import org.hisp.dhis.android.core.relationship.RelationshipConstraint; -import org.hisp.dhis.android.core.relationship.RelationshipConstraintType; -import org.hisp.dhis.android.core.relationship.RelationshipType; - -import java.util.Set; - -public class RelationshipTypeBuilder { - - private final Set constraints; - - RelationshipTypeBuilder(Set constraints) { - this.constraints = constraints; - } - - public RelationshipType typeWithConstraints(RelationshipType type) { - - RelationshipType.Builder typeBuilder = type.toBuilder(); - - for (RelationshipConstraint constraint : this.constraints) { - if (constraint.relationshipType().uid().equals(type.uid())) { - if (constraint.constraintType().equals(RelationshipConstraintType.FROM)) { - typeBuilder.fromConstraint(constraint); - } else if (constraint.constraintType().equals(RelationshipConstraintType.TO)) { - typeBuilder.toConstraint(constraint); - } - } - } - - return typeBuilder.build(); - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index 30c0794e05..626993b509 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -68,7 +68,6 @@ internal class NewTrackedEntityEndpointCallFactory( page = query.page, pageSize = query.pageSize, lastUpdatedStartDate = query.lastUpdatedStr, - includeAllAttributes = true, includeDeleted = true, ).let { mapPayload(it) } } @@ -81,7 +80,6 @@ internal class NewTrackedEntityEndpointCallFactory( program = query.commonParams.program, programStatus = getProgramStatus(query), programStartDate = getProgramStartDate(query), - includeAllAttributes = true, includeDeleted = true, ).let { NewTrackerImporterTrackedEntityTransformer.deTransform(it) } } @@ -91,7 +89,6 @@ internal class NewTrackedEntityEndpointCallFactory( trackedEntityInstance = uid, fields = NewTrackedEntityInstanceFields.asRelationshipFields, orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, - includeAllAttributes = true, includeDeleted = true, ).let { mapPayload(it) } } @@ -199,7 +196,6 @@ internal class NewTrackedEntityEndpointCallFactory( paging = query.paging, page = query.page, pageSize = query.pageSize, - includeAllAttributes = true, ) mapPayload(payload) diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index cd493a39d6..be574c531b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -42,7 +42,6 @@ internal interface TrackerExporterService { @Query(FIELDS) @Which fields: Fields, @Query(TRACKED_ENTITY_INSTACE) trackedEntityInstance: String?, @Query(OU_MODE) orgUnitMode: String?, - @Query(INCLUDE_ALL_ATTRIBUTES) includeAllAttributes: Boolean, @Query(INCLUDE_DELETED) includeDeleted: Boolean, ): NTIPayload @@ -54,7 +53,6 @@ internal interface TrackerExporterService { @Query(PROGRAM) program: String?, @Query(PROGRAM_STATUS) programStatus: String?, @Query(ENROLLMENT_ENROLLED_AFTER) programStartDate: String?, - @Query(INCLUDE_ALL_ATTRIBUTES) includeAllAttributes: Boolean, @Query(INCLUDE_DELETED) includeDeleted: Boolean, ): NewTrackerImporterTrackedEntity @@ -85,7 +83,6 @@ internal interface TrackerExporterService { @Query(PAGING) paging: Boolean, @Query(PAGE) page: Int, @Query(PAGE_SIZE) pageSize: Int, - @Query(INCLUDE_ALL_ATTRIBUTES) includeAllAttributes: Boolean, @Query(INCLUDE_DELETED) includeDeleted: Boolean = false, ): NTIPayload @@ -164,7 +161,6 @@ internal interface TrackerExporterService { const val SCHEDULED_AFTER = "scheduledAfter" const val SCHEDULED_BEFORE = "scheduledBefore" const val TRACKED_ENTITY_TYPE = "trackedEntityType" - const val INCLUDE_ALL_ATTRIBUTES = "includeAllAttributes" const val FILTER = "filter" const val FILTER_ATTRIBUTES = "filterAttributes" const val UPDATED_AFTER = "updatedAfter" From 57952fb4414ef252b4e5062b9456e16dee2464e6 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 10 Jan 2024 10:42:37 +0100 Subject: [PATCH 017/222] [ANDROSDK-1695] PMD, Detekt and Checkstyle --- .../collection/RelationshipTypeAsserts.kt | 32 ++++++++++++++----- .../RelationshipConstraintFields.java | 1 - 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt index 0a3ec31418..9ab2cababa 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/RelationshipTypeAsserts.kt @@ -52,18 +52,34 @@ internal object RelationshipTypeAsserts : BaseRealIntegrationTest() { fun assertTypeWithConstraints(target: RelationshipType, reference: RelationshipType) { val prunedTarget = target.toBuilder() .id(null) - .toConstraint(target.toConstraint()?.toBuilder()?.id(null)?. - trackerDataView(target.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) - .fromConstraint(target.fromConstraint()?.toBuilder()?.id(null)?. - trackerDataView(target.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) + .toConstraint( + target.toConstraint()?.toBuilder()?.id(null) + ?.trackerDataView( + target.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build(), + )?.build(), + ) + .fromConstraint( + target.fromConstraint()?.toBuilder()?.id(null) + ?.trackerDataView( + target.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build(), + )?.build(), + ) .build() val prunedReference = reference.toBuilder() .id(null) - .toConstraint(reference.toConstraint()?.toBuilder()?.id(null)?. - trackerDataView(reference.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) - .fromConstraint(reference.fromConstraint()?.toBuilder()?.id(null)?. - trackerDataView(reference.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build())?.build()) + .toConstraint( + reference.toConstraint()?.toBuilder()?.id(null) + ?.trackerDataView( + reference.toConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build(), + )?.build(), + ) + .fromConstraint( + reference.fromConstraint()?.toBuilder()?.id(null) + ?.trackerDataView( + reference.fromConstraint()?.trackerDataView()?.toBuilder()?.id(null)?.build(), + )?.build(), + ) .build() assertThat(prunedTarget).isEqualTo(prunedReference) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java index 66d1c2dd37..d96455dec7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipConstraintFields.java @@ -30,7 +30,6 @@ import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.dataset.internal.SectionFields; import org.hisp.dhis.android.core.relationship.RelationshipConstraint; import org.hisp.dhis.android.core.relationship.RelationshipConstraintTableInfo.Columns; import org.hisp.dhis.android.core.relationship.RelationshipEntityType; From 6c8e7ff917f76a38959498851ab6ec1a912274f2 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 10 Jan 2024 12:39:09 +0100 Subject: [PATCH 018/222] [ANDROSDK-1693] Include 'program' parameter in related tei download --- .../internal/EnrollmentEndpointCallFactory.kt | 3 +- .../NewEnrollmentEndpointCallFactory.kt | 7 +- .../OldEnrollmentEndpointCallFactory.kt | 7 +- .../internal/EventEndpointCallFactory.kt | 3 +- .../internal/NewEventEndpointCallFactory.kt | 5 +- .../internal/OldEventEndpointCallFactory.kt | 5 +- .../RelationshipDHISVersionManager.java | 129 ------------------ .../RelationshipDHISVersionManager.kt | 96 +++++++++++++ ...lationshipDownloadAndPersistCallFactory.kt | 43 +++--- .../internal/RelationshipItemRelative.kt | 36 +++++ ...ives.java => RelationshipItemRelatives.kt} | 45 +++--- .../NewTrackedEntityEndpointCallFactory.kt | 29 +++- .../OldTrackedEntityEndpointCallFactory.kt | 5 +- .../TrackedEntityEndpointCallFactory.kt | 3 +- .../TrackedEntityInstanceImportHandler.kt | 22 +-- .../exporter/TrackerExporterService.kt | 8 -- .../TrackedEntityInstanceHandlerShould.kt | 3 - ...rackedEntityInstanceImportHandlerShould.kt | 12 +- 18 files changed, 223 insertions(+), 238 deletions(-) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt rename core/src/main/java/org/hisp/dhis/android/core/relationship/internal/{RelationshipItemRelatives.java => RelationshipItemRelatives.kt} (59%) diff --git a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentEndpointCallFactory.kt index 2cf0628095..77a9095673 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/EnrollmentEndpointCallFactory.kt @@ -28,7 +28,8 @@ package org.hisp.dhis.android.core.enrollment.internal import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative internal interface EnrollmentEndpointCallFactory { - suspend fun getRelationshipEntityCall(uid: String): Enrollment + suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Enrollment } diff --git a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/NewEnrollmentEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/NewEnrollmentEndpointCallFactory.kt index 03dbf2f46b..75eb0d6e66 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/NewEnrollmentEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/NewEnrollmentEndpointCallFactory.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.enrollment.internal import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.NewTrackerImporterEnrollmentTransformer +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterService import org.koin.core.annotation.Singleton @@ -36,10 +37,10 @@ import org.koin.core.annotation.Singleton internal class NewEnrollmentEndpointCallFactory( private val service: TrackerExporterService, ) : EnrollmentEndpointCallFactory { - override suspend fun getRelationshipEntityCall(uid: String): Enrollment { + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Enrollment { return service.getEnrollmentSingle( - uid, - NewEnrollmentFields.asRelationshipFields, + enrollmentUid = item.itemUid, + fields = NewEnrollmentFields.asRelationshipFields, ).let { NewTrackerImporterEnrollmentTransformer.deTransform(it) } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/OldEnrollmentEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/OldEnrollmentEndpointCallFactory.kt index 8383e5421c..130008a7aa 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/OldEnrollmentEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/enrollment/internal/OldEnrollmentEndpointCallFactory.kt @@ -28,16 +28,17 @@ package org.hisp.dhis.android.core.enrollment.internal import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.koin.core.annotation.Singleton @Singleton internal class OldEnrollmentEndpointCallFactory( private val service: EnrollmentService, ) : EnrollmentEndpointCallFactory { - override suspend fun getRelationshipEntityCall(uid: String): Enrollment { + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Enrollment { return service.getEnrollmentSingle( - uid, - EnrollmentFields.asRelationshipFields, + enrollmentUid = item.itemUid, + fields = EnrollmentFields.asRelationshipFields, ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventEndpointCallFactory.kt index 46818056ca..babada4742 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventEndpointCallFactory.kt @@ -29,13 +29,14 @@ package org.hisp.dhis.android.core.event.internal import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery internal abstract class EventEndpointCallFactory { abstract suspend fun getCollectionCall(eventQuery: TrackerAPIQuery): Payload - abstract suspend fun getRelationshipEntityCall(uid: String): Payload + abstract suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload protected fun getUidStr(query: TrackerAPIQuery): String? { return if (query.uids.isEmpty()) null else query.uids.joinToString(";") diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt index 9f5a6f59cc..c25e7d2bb8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.NewTrackerImporterEvent import org.hisp.dhis.android.core.event.NewTrackerImporterEventTransformer import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterService import org.koin.core.annotation.Singleton @@ -58,9 +59,9 @@ internal class NewEventEndpointCallFactory( ).let { mapPayload(it) } } - override suspend fun getRelationshipEntityCall(uid: String): Payload { + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { return service.getEventSingle( - eventUid = uid, + eventUid = item.itemUid, fields = NewEventFields.asRelationshipFields, orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, ).let { mapPayload(it) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt index 8d5bc99f39..efa98b269d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.event.internal import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery import org.koin.core.annotation.Singleton @@ -54,9 +55,9 @@ internal class OldEventEndpointCallFactory( ) } - override suspend fun getRelationshipEntityCall(uid: String): Payload { + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { return service.getEventSingle( - eventUid = uid, + eventUid = item.itemUid, fields = EventFields.asRelationshipFields, orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.java deleted file mode 100644 index 71229dc69f..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.relationship.internal; - -import org.hisp.dhis.android.core.relationship.BaseRelationship; -import org.hisp.dhis.android.core.relationship.Relationship; -import org.hisp.dhis.android.core.relationship.RelationshipHelper; -import org.hisp.dhis.android.core.relationship.RelationshipItem; -import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo; -import org.hisp.dhis.android.core.relationship.RelationshipType; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceInternalAccessor; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -public class RelationshipDHISVersionManager { - - private final RelationshipTypeStore relationshipTypeStore; - - public RelationshipDHISVersionManager(RelationshipTypeStore relationshipTypeStore) { - this.relationshipTypeStore = relationshipTypeStore; - } - - public List getOwnedRelationships(Collection relationships, String elementUid) { - List ownedRelationships = new ArrayList<>(); - for (Relationship relationship : relationships) { - RelationshipItem fromItem = relationship.from(); - if (isBidirectional(relationship) || - fromItem != null && fromItem.elementUid() != null && fromItem.elementUid().equals(elementUid)) { - ownedRelationships.add(relationship); - } - } - return ownedRelationships; - } - - private boolean isBidirectional(Relationship relationship) { - if (relationship.relationshipType() == null) { - return false; - } else { - RelationshipType relationshipType = relationshipTypeStore.selectByUid(relationship.relationshipType()); - return relationshipType != null && relationshipType.bidirectional(); - } - } - - public TrackedEntityInstance getRelativeTei(Relationship relationship, String teiUid) { - return getRelativeTEI230(relationship, teiUid); - } - - public TrackedEntityInstance getRelativeTEI230(BaseRelationship baseRelationship, String teiUid) { - String fromTEIUid = RelationshipHelper.getTeiUid(baseRelationship.from()); - String toTEIUid = RelationshipHelper.getTeiUid(baseRelationship.to()); - - if (fromTEIUid == null || toTEIUid == null) { - return null; - } - - String relatedTEIUid = teiUid.equals(fromTEIUid) ? toTEIUid : fromTEIUid; - - return TrackedEntityInstanceInternalAccessor.insertRelationships( - TrackedEntityInstance.builder(), Collections.emptyList()) - .uid(relatedTEIUid) - .deleted(false) - .build(); - } - - public RelationshipItem getRelatedRelationshipItem(BaseRelationship baseRelationship, String parentUid) { - String fromUid = baseRelationship.from() == null ? null : baseRelationship.from().elementUid(); - String toUid = baseRelationship.to() == null ? null : baseRelationship.to().elementUid(); - - if (fromUid == null || toUid == null) { - return null; - } - - return parentUid.equals(fromUid) ? baseRelationship.to() : baseRelationship.from(); - } - - public void saveRelativesIfNotExist(Collection relationships, - String parentUid, - RelationshipItemRelatives relatives, - RelationshipHandler relationshipHandler) { - for (BaseRelationship relationship : relationships) { - RelationshipItem item = getRelatedRelationshipItem(relationship, parentUid); - if (item != null && !relationshipHandler.doesRelationshipItemExist(item)) { - switch (item.elementType()) { - case RelationshipItemTableInfo.Columns.TRACKED_ENTITY_INSTANCE: - relatives.addTrackedEntityInstance(item.elementUid()); - break; - case RelationshipItemTableInfo.Columns.ENROLLMENT: - relatives.addEnrollment(item.elementUid()); - break; - case RelationshipItemTableInfo.Columns.EVENT: - relatives.addEvent(item.elementUid()); - break; - default: - break; - } - } - } - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt new file mode 100644 index 0000000000..08a673ac86 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.relationship.internal + +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.relationship.BaseRelationship +import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipItem +import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.ENROLLMENT +import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.EVENT +import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.TRACKED_ENTITY_INSTANCE + +internal class RelationshipDHISVersionManager( + private val relationshipTypeStore: RelationshipTypeStore +) { + fun getOwnedRelationships(relationships: Collection, elementUid: String): List { + return relationships.filter { relationship -> + val fromItem = relationship.from() + isBidirectional(relationship) || fromItem?.elementUid() == elementUid + } + } + + private fun isBidirectional(relationship: Relationship): Boolean { + return relationship.relationshipType()?.let { relationshipTypeUid -> + relationshipTypeStore.selectByUid(relationshipTypeUid)?.bidirectional() + } ?: false + } + + private fun getRelatedRelationshipItem(baseRelationship: BaseRelationship, parentUid: String): RelationshipItem? { + val fromUid = baseRelationship.from()?.elementUid() ?: return null + val toUid = baseRelationship.to()?.elementUid() ?: return null + + val itemBuilder = + if (parentUid == fromUid) { + baseRelationship.to()?.toBuilder() + ?.relationshipItemType(RelationshipConstraintType.TO) + } else { + baseRelationship.from()?.toBuilder() + ?.relationshipItemType(RelationshipConstraintType.FROM) + } + + return itemBuilder + ?.relationship(ObjectWithUid.create(baseRelationship.uid())) + ?.build() + } + + fun saveRelativesIfNotExist( + relationships: Collection, + parentUid: String, + relatives: RelationshipItemRelatives, + relationshipHandler: RelationshipHandler + ) { + for (relationship in relationships) { + val item = getRelatedRelationshipItem(relationship, parentUid) + if (item != null && !relationshipHandler.doesRelationshipItemExist(item)) { + val relationshipItem = RelationshipItemRelative( + itemUid = item.elementUid(), + relationshipTypeUid = relationship.relationshipType() ?: continue, + itemType = item.relationshipItemType() ?: continue + ) + when (item.elementType()) { + TRACKED_ENTITY_INSTANCE -> relatives.addTrackedEntityInstance(relationshipItem) + ENROLLMENT -> relatives.addEnrollment(relationshipItem) + EVENT -> relatives.addEvent(relationshipItem) + else -> {} + } + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt index fea875c513..09da6308c1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt @@ -57,21 +57,24 @@ internal class RelationshipDownloadAndPersistCallFactory( val events: MutableList = mutableListOf() val failedEvents: MutableList = mutableListOf() - for (uid in relatives.relativeEventUids) { + for (item in relatives.getRelativeEvents()) { coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getEventCall().getRelationshipEntityCall(uid) + trackerParentCallFactory.getEventCall().getRelationshipEntityCall(item) }.fold( - onSuccess = { eventPayload -> events.addAll(eventPayload.items()) }, - onFailure = { failedEvents.add(uid) }, + onSuccess = { eventPayload -> + events.addAll(eventPayload.items()) + eventPayload.items().mapNotNull { it.enrollment() }.forEach { enrollment -> + val relativeEnrollment = + RelationshipItemRelative(enrollment, item.relationshipTypeUid, item.itemType) + relatives.addEnrollment(relativeEnrollment) + } + }, + onFailure = { failedEvents.add(item.itemUid) }, ) } eventPersistenceCallFactory.persistAsRelationships(events) - events - .mapNotNull { it.enrollment() } - .forEach { relatives.addEnrollment(it) } - cleanFailedRelationships(failedEvents, RelationshipItemTableInfo.Columns.EVENT) } @@ -79,21 +82,23 @@ internal class RelationshipDownloadAndPersistCallFactory( val enrollments: MutableList = mutableListOf() val failedEnrollments: MutableList = mutableListOf() - for (uid in relatives.relativeEnrollmentUids) { + for (item in relatives.getRelativeEnrollments()) { coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getEnrollmentCall().getRelationshipEntityCall(uid) + trackerParentCallFactory.getEnrollmentCall().getRelationshipEntityCall(item) }.fold( - onSuccess = { enrollment -> enrollments.add(enrollment) }, - onFailure = { failedEnrollments.add(uid) }, + onSuccess = { enrollment -> + enrollments.add(enrollment) + enrollment.trackedEntityInstance()?.let { tei -> + val relativeTei = RelationshipItemRelative(tei, item.relationshipTypeUid, item.itemType) + relatives.addTrackedEntityInstance(relativeTei) + } + }, + onFailure = { failedEnrollments.add(item.itemUid) }, ) } enrollmentPersistenceCallFactory.persistAsRelationships(enrollments).blockingAwait() - enrollments - .mapNotNull { it.trackedEntityInstance() } - .forEach { relatives.addTrackedEntityInstance(it) } - cleanFailedRelationships(failedEnrollments, RelationshipItemTableInfo.Columns.ENROLLMENT) } @@ -101,12 +106,12 @@ internal class RelationshipDownloadAndPersistCallFactory( val teis: MutableList = mutableListOf() val failedTeis: MutableList = mutableListOf() - for (uid in relatives.relativeTrackedEntityInstanceUids) { + for (item in relatives.getRelativeTrackedEntityInstances()) { coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getTrackedEntityCall().getRelationshipEntityCall(uid) + trackerParentCallFactory.getTrackedEntityCall().getRelationshipEntityCall(item) }.fold( onSuccess = { teiPayload -> teis.addAll(teiPayload.items()) }, - onFailure = { failedTeis.add(uid) }, + onFailure = { failedTeis.add(item.itemUid) }, ) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt new file mode 100644 index 0000000000..c73d6d0b5d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.relationship.internal + +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType + +data class RelationshipItemRelative( + val itemUid: String, + val relationshipTypeUid: String, + val itemType: RelationshipConstraintType +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt similarity index 59% rename from core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.java rename to core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt index 970ea9b1e3..28961229da 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt @@ -25,45 +25,34 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.relationship.internal -package org.hisp.dhis.android.core.relationship.internal; +class RelationshipItemRelatives { + private val relativeTrackedEntityInstanceItems: MutableSet = mutableSetOf() + private val relativeEnrollmentItems: MutableSet = mutableSetOf() + private val relativeEventItems: MutableSet = mutableSetOf() -import java.util.HashSet; -import java.util.Set; - -public class RelationshipItemRelatives { - - private final Set relativeTrackedEntityInstanceUids; - private final Set relativeEnrollmentUids; - private final Set relativeEventUids; - - public RelationshipItemRelatives() { - this.relativeTrackedEntityInstanceUids = new HashSet<>(); - this.relativeEnrollmentUids = new HashSet<>(); - this.relativeEventUids = new HashSet<>(); - } - - public void addTrackedEntityInstance(String uid) { - this.relativeTrackedEntityInstanceUids.add(uid); + fun addTrackedEntityInstance(uid: RelationshipItemRelative) { + relativeTrackedEntityInstanceItems.add(uid) } - public void addEnrollment(String uid) { - this.relativeEnrollmentUids.add(uid); + fun addEnrollment(uid: RelationshipItemRelative) { + relativeEnrollmentItems.add(uid) } - public void addEvent(String uid) { - this.relativeEventUids.add(uid); + fun addEvent(uid: RelationshipItemRelative) { + relativeEventItems.add(uid) } - public Set getRelativeTrackedEntityInstanceUids() { - return relativeTrackedEntityInstanceUids; + fun getRelativeTrackedEntityInstances(): Set { + return relativeTrackedEntityInstanceItems } - public Set getRelativeEnrollmentUids() { - return relativeEnrollmentUids; + fun getRelativeEnrollments(): Set { + return relativeEnrollmentItems } - public Set getRelativeEventUids() { - return relativeEventUids; + fun getRelativeEvents(): Set { + return relativeEventItems } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index 626993b509..142c4c4659 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -33,6 +33,9 @@ import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.event.NewTrackerImporterEvent import org.hisp.dhis.android.core.event.internal.NewEventFields import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipTypeCollectionRepository +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.trackedentity.NewTrackerImporterTrackedEntity import org.hisp.dhis.android.core.trackedentity.NewTrackerImporterTrackedEntityTransformer import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance @@ -52,6 +55,7 @@ import org.koin.core.annotation.Singleton internal class NewTrackedEntityEndpointCallFactory( private val trackedExporterService: TrackerExporterService, private val coroutineAPICallExecutor: CoroutineAPICallExecutor, + private val relationshipTypeRepository: RelationshipTypeCollectionRepository, ) : TrackedEntityEndpointCallFactory() { override suspend fun getCollectionCall(query: TrackerAPIQuery): Payload { @@ -84,13 +88,16 @@ internal class NewTrackedEntityEndpointCallFactory( ).let { NewTrackerImporterTrackedEntityTransformer.deTransform(it) } } - override suspend fun getRelationshipEntityCall(uid: String): Payload { - return trackedExporterService.getTrackedEntityInstance( - trackedEntityInstance = uid, + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { + return trackedExporterService.getSingleTrackedEntityInstance( fields = NewTrackedEntityInstanceFields.asRelationshipFields, + trackedEntityInstanceUid = item.itemUid, orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, + program = getRelatedProgramUid(item), + programStatus = null, + programStartDate = null, includeDeleted = true, - ).let { mapPayload(it) } + ).let { Payload(listOf(NewTrackerImporterTrackedEntityTransformer.deTransform(it))) } } override suspend fun getQueryCall(query: TrackedEntityInstanceQueryOnline): TrackerQueryResult { @@ -234,4 +241,18 @@ internal class NewTrackedEntityEndpointCallFactory( orgUnits.joinToString(";") } } + + private fun getRelatedProgramUid(item: RelationshipItemRelative): String? { + val relationshipType = relationshipTypeRepository + .withConstraints() + .uid(item.relationshipTypeUid) + .blockingGet() + + val constraint = when(item.itemType) { + RelationshipConstraintType.FROM -> relationshipType?.fromConstraint() + RelationshipConstraintType.TO -> relationshipType?.toConstraint() + } + + return constraint?.program()?.uid() + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt index 0144eaace3..bc5ad5cffe 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.trackedentity.internal import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryCallFactory import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline @@ -76,10 +77,10 @@ internal class OldTrackedEntityEndpointCallFactory( ) } - override suspend fun getRelationshipEntityCall(uid: String): Payload { + override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { return trackedEntityInstanceService.getTrackedEntityInstance( fields = TrackedEntityInstanceFields.asRelationshipFields, - trackedEntityInstance = uid, + trackedEntityInstance = item.itemUid, orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, includeAllAttributes = true, includeDeleted = true, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt index 96b323f0ba..949ecb8da5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.trackedentity.internal import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult @@ -40,7 +41,7 @@ internal abstract class TrackedEntityEndpointCallFactory { abstract suspend fun getEntityCall(uid: String, query: TrackerAPIQuery): TrackedEntityInstance - abstract suspend fun getRelationshipEntityCall(uid: String): Payload + abstract suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload abstract suspend fun getQueryCall(query: TrackedEntityInstanceQueryOnline): TrackerQueryResult diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandler.kt index d6414359e5..7f356f8c0e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandler.kt @@ -38,16 +38,12 @@ import org.hisp.dhis.android.core.imports.internal.TEIImportSummary import org.hisp.dhis.android.core.imports.internal.TEIWebResponseHandlerSummary import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictParser import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictStore -import org.hisp.dhis.android.core.relationship.RelationshipCollectionRepository -import org.hisp.dhis.android.core.relationship.RelationshipHelper -import org.hisp.dhis.android.core.relationship.internal.RelationshipDHISVersionManager -import org.hisp.dhis.android.core.relationship.internal.RelationshipStore import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceInternalAccessor import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceTableInfo import org.hisp.dhis.android.core.tracker.importer.internal.JobReportTrackedEntityHandler import org.koin.core.annotation.Singleton -import java.util.* +import java.util.Date @Singleton internal class TrackedEntityInstanceImportHandler internal constructor( @@ -55,10 +51,7 @@ internal class TrackedEntityInstanceImportHandler internal constructor( private val enrollmentImportHandler: EnrollmentImportHandler, private val trackerImportConflictStore: TrackerImportConflictStore, private val trackerImportConflictParser: TrackerImportConflictParser, - private val relationshipStore: RelationshipStore, private val dataStatePropagator: DataStatePropagator, - private val relationshipDHISVersionManager: RelationshipDHISVersionManager, - private val relationshipRepository: RelationshipCollectionRepository, private val jobReportTrackedEntityHandler: JobReportTrackedEntityHandler, ) { @@ -89,7 +82,6 @@ internal class TrackedEntityInstanceImportHandler internal constructor( resetNestedDataStates(instance) instance?.let { summary.teis.error.add(it) } } else { - setRelationshipsState(teiUid, State.SYNCED) instance?.let { summary.teis.success.add(it) } } @@ -152,17 +144,6 @@ internal class TrackedEntityInstanceImportHandler internal constructor( trackerImportConflicts.forEach { trackerImportConflictStore.insert(it) } } - // Legacy code for <= 2.29 - private fun setRelationshipsState(trackedEntityInstanceUid: String?, state: State) { - val dbRelationships = - relationshipRepository.getByItem(RelationshipHelper.teiItem(trackedEntityInstanceUid), true, false) - val ownedRelationships = relationshipDHISVersionManager - .getOwnedRelationships(dbRelationships, trackedEntityInstanceUid) - for (relationship in ownedRelationships) { - relationshipStore.setSyncStateOrDelete(relationship.uid()!!, state) - } - } - private fun processIgnoredTEIs( processedTEIs: List, instances: List, @@ -177,7 +158,6 @@ internal class TrackedEntityInstanceImportHandler internal constructor( private fun resetNestedDataStates(instance: TrackedEntityInstance?) { instance?.let { dataStatePropagator.resetUploadingEnrollmentAndEventStates(instance.uid()) - setRelationshipsState(instance.uid(), State.TO_UPDATE) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index be574c531b..285e22cfb5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -37,14 +37,6 @@ import retrofit2.http.* @Suppress("LongParameterList") internal interface TrackerExporterService { - @GET(TRACKED_ENTITY_INSTANCES) - suspend fun getTrackedEntityInstance( - @Query(FIELDS) @Which fields: Fields, - @Query(TRACKED_ENTITY_INSTACE) trackedEntityInstance: String?, - @Query(OU_MODE) orgUnitMode: String?, - @Query(INCLUDE_DELETED) includeDeleted: Boolean, - ): NTIPayload - @GET("$TRACKED_ENTITY_INSTANCES/{$TRACKED_ENTITY_INSTACE}") suspend fun getSingleTrackedEntityInstance( @Path(TRACKED_ENTITY_INSTACE) trackedEntityInstanceUid: String, diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt index afbc6eabb8..9782dbee60 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt @@ -182,7 +182,6 @@ class TrackedEntityInstanceHandlerShould { @Test fun invoke_relationship_handler_with_relationship_from_version_manager() { - whenever(relationshipVersionManager.getRelativeTei(relationship, TEI_UID)).doReturn(relative) whenever(relationshipVersionManager.getOwnedRelationships(listOf(relationship), TEI_UID)) .doReturn(listOf(relationship)) whenever(relative.toBuilder()).doReturn(relativeBuilder) @@ -203,8 +202,6 @@ class TrackedEntityInstanceHandlerShould { @Test fun do_not_invoke_relationship_repository_when_no_relative() { - whenever(relationshipVersionManager.getRelativeTei(relationship, TEI_UID)).doReturn(null) - trackedEntityInstanceHandler.handleMany(listOf(trackedEntityInstance), params, relationshipItemRelatives) verify(relationshipHandler, never()).handle(any()) diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandlerShould.kt index 9723814c6f..1759c527b2 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceImportHandlerShould.kt @@ -35,9 +35,6 @@ import org.hisp.dhis.android.core.common.internal.DataStatePropagator import org.hisp.dhis.android.core.enrollment.internal.EnrollmentImportHandler import org.hisp.dhis.android.core.imports.ImportStatus import org.hisp.dhis.android.core.imports.internal.* -import org.hisp.dhis.android.core.relationship.RelationshipCollectionRepository -import org.hisp.dhis.android.core.relationship.internal.RelationshipDHISVersionManager -import org.hisp.dhis.android.core.relationship.internal.RelationshipStore import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.tracker.importer.internal.JobReportTrackedEntityHandler @@ -64,14 +61,8 @@ class TrackedEntityInstanceImportHandlerShould { private val trackerImportConflictParser: TrackerImportConflictParser = mock() - private val relationshipStore: RelationshipStore = mock() - private val dataStatePropagator: DataStatePropagator = mock() - private val relationshipDHISVersionManager: RelationshipDHISVersionManager = mock() - - private val relationshipCollectionRepository: RelationshipCollectionRepository = mock() - private val jobReportTrackedEntityHandler: JobReportTrackedEntityHandler = mock() private val trackedEntityInstance: TrackedEntityInstance = mock() @@ -87,8 +78,7 @@ class TrackedEntityInstanceImportHandlerShould { fun setUp() { trackedEntityInstanceImportHandler = TrackedEntityInstanceImportHandler( trackedEntityInstanceStore, enrollmentImportHandler, trackerImportConflictStore, - trackerImportConflictParser, relationshipStore, dataStatePropagator, relationshipDHISVersionManager, - relationshipCollectionRepository, jobReportTrackedEntityHandler, + trackerImportConflictParser, dataStatePropagator, jobReportTrackedEntityHandler, ) whenever(trackedEntityInstanceStore.setSyncStateOrDelete(any(), any())).doReturn(HandleAction.Update) From 60a070bf7971d969ebd3f3ae1ac5b0ebfa17f4f2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Thu, 11 Jan 2024 10:15:16 +0100 Subject: [PATCH 019/222] [ANDROSDK-1695] Fix SonarCloud issues --- .../hisp/dhis/android/core/relationship/TrackerDataView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java index 476f108c5c..96fef02cd4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/TrackerDataView.java @@ -59,7 +59,7 @@ public abstract class TrackerDataView extends BaseObject { public abstract List dataElements(); public static TrackerDataView create(Cursor cursor) { - return AutoValue_TrackerDataView.createFromCursor(cursor); + return $AutoValue_TrackerDataView.createFromCursor(cursor); } public static Builder builder() { @@ -70,7 +70,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder attributes(List attributes); From 1f2f25e1d6234eca020d02675482c6ede509fc9a Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Thu, 11 Jan 2024 10:27:41 +0100 Subject: [PATCH 020/222] [ANDROSDK-1695] Reorder the modifiers to comply with the Java Language Specification. --- .../android/core/arch/call/queries/internal/BaseQuery.java | 2 +- .../core/attribute/DataElementAttributeValueLink.java | 2 +- .../android/core/attribute/ProgramAttributeValueLink.java | 2 +- .../core/attribute/ProgramStageAttributeValueLink.java | 2 +- .../java/org/hisp/dhis/android/core/category/Category.java | 2 +- .../android/core/category/CategoryCategoryComboLink.java | 2 +- .../android/core/category/CategoryCategoryOptionLink.java | 2 +- .../org/hisp/dhis/android/core/category/CategoryCombo.java | 2 +- .../org/hisp/dhis/android/core/category/CategoryOption.java | 2 +- .../dhis/android/core/category/CategoryOptionCombo.java | 2 +- .../category/CategoryOptionComboCategoryOptionLink.java | 2 +- .../core/category/CategoryOptionOrganisationUnitLink.java | 2 +- .../org/hisp/dhis/android/core/common/BaseDataObject.java | 2 +- .../dhis/android/core/common/BaseDeletableDataObject.java | 2 +- .../dhis/android/core/common/BaseIdentifiableObject.java | 2 +- .../hisp/dhis/android/core/common/BaseNameableObject.java | 2 +- .../java/org/hisp/dhis/android/core/common/BaseObject.java | 2 +- .../org/hisp/dhis/android/core/common/FilterOperators.java | 2 +- .../hisp/dhis/android/core/common/FilterQueryCriteria.java | 2 +- .../org/hisp/dhis/android/core/dataelement/DataElement.java | 2 +- .../dhis/android/core/dataelement/DataElementOperand.java | 2 +- .../java/org/hisp/dhis/android/core/dataset/DataSet.java | 2 +- .../dataset/DataSetCompulsoryDataElementOperandLink.java | 2 +- .../org/hisp/dhis/android/core/dataset/DataSetElement.java | 2 +- .../android/core/dataset/DataSetOrganisationUnitLink.java | 2 +- .../java/org/hisp/dhis/android/core/dataset/Section.java | 2 +- .../dhis/android/core/dataset/SectionDataElementLink.java | 2 +- .../dhis/android/core/dataset/SectionGreyedFieldsLink.java | 2 +- .../android/core/dataset/internal/SectionIndicatorLink.java | 2 +- .../org/hisp/dhis/android/core/datastore/KeyValuePair.java | 2 +- .../hisp/dhis/android/core/datavalue/DataValueConflict.java | 2 +- .../domain/aggregated/data/internal/AggregatedDataSync.java | 2 +- .../org/hisp/dhis/android/core/event/EventDataFilter.java | 2 +- .../java/org/hisp/dhis/android/core/event/EventFilter.java | 2 +- .../hisp/dhis/android/core/event/EventQueryCriteria.java | 2 +- .../hisp/dhis/android/core/event/internal/EventSync.java | 2 +- .../dhis/android/core/imports/TrackerImportConflict.java | 2 +- .../android/core/imports/internal/BaseImportSummaries.java | 2 +- .../android/core/imports/internal/BaseImportSummary.java | 2 +- .../core/imports/internal/EnrollmentImportSummaries.java | 2 +- .../core/imports/internal/EnrollmentImportSummary.java | 2 +- .../android/core/imports/internal/EventImportSummaries.java | 2 +- .../android/core/imports/internal/EventImportSummary.java | 2 +- .../android/core/imports/internal/EventWebResponse.java | 2 +- .../android/core/imports/internal/HttpMessageResponse.java | 2 +- .../core/imports/internal/RelationshipDeleteSummary.java | 2 +- .../imports/internal/RelationshipDeleteWebResponse.java | 2 +- .../core/imports/internal/RelationshipImportSummaries.java | 2 +- .../core/imports/internal/RelationshipImportSummary.java | 2 +- .../core/imports/internal/RelationshipWebResponse.java | 2 +- .../android/core/imports/internal/TEIImportSummaries.java | 2 +- .../android/core/imports/internal/TEIImportSummary.java | 2 +- .../dhis/android/core/imports/internal/TEIWebResponse.java | 2 +- .../dhis/android/core/imports/internal/WebResponse.java | 2 +- .../org/hisp/dhis/android/core/indicator/Indicator.java | 2 +- .../org/hisp/dhis/android/core/indicator/IndicatorType.java | 2 +- .../android/core/legendset/DataElementLegendSetLink.java | 2 +- .../dhis/android/core/legendset/IndicatorLegendSetLink.java | 2 +- .../core/legendset/ProgramIndicatorLegendSetLink.java | 2 +- .../org/hisp/dhis/android/core/maintenance/D2Error.java | 2 +- .../dhis/android/core/maintenance/ForeignKeyViolation.java | 2 +- .../hisp/dhis/android/core/note/NewTrackerImporterNote.java | 2 +- .../src/main/java/org/hisp/dhis/android/core/note/Note.java | 2 +- .../hisp/dhis/android/core/note/NoteCreateProjection.java | 2 +- .../main/java/org/hisp/dhis/android/core/option/Option.java | 2 +- .../java/org/hisp/dhis/android/core/option/OptionGroup.java | 2 +- .../dhis/android/core/option/OptionGroupOptionLink.java | 2 +- .../java/org/hisp/dhis/android/core/option/OptionSet.java | 2 +- .../OrganisationUnitOrganisationUnitGroupLink.java | 2 +- .../core/organisationunit/OrganisationUnitProgramLink.java | 2 +- .../java/org/hisp/dhis/android/core/program/Program.java | 2 +- .../org/hisp/dhis/android/core/program/ProgramRule.java | 2 +- .../android/core/program/ProgramSectionAttributeLink.java | 2 +- .../org/hisp/dhis/android/core/program/ProgramStage.java | 2 +- .../core/program/ProgramStageSectionDataElementLink.java | 2 +- .../program/ProgramStageSectionProgramIndicatorLink.java | 2 +- .../programstageworkinglist/ProgramStageQueryCriteria.java | 2 +- .../programstageworkinglist/ProgramStageWorkingList.java | 2 +- .../ProgramStageWorkingListAttributeValueFilter.java | 2 +- .../ProgramStageWorkingListEventDataFilter.java | 2 +- .../android/core/relationship/RelationshipConstraint.java | 2 +- .../dhis/android/core/relationship/RelationshipType.java | 2 +- .../sms/data/localdbrepository/internal/SMSMetadataId.java | 2 +- .../localdbrepository/internal/SMSOngoingSubmission.java | 2 +- .../data/webapirepository/internal/MetadataResponse.java | 6 +++--- .../org/hisp/dhis/android/core/systeminfo/SystemInfo.java | 2 +- .../android/core/trackedentity/AttributeValueFilter.java | 2 +- .../android/core/trackedentity/EntityQueryCriteria.java | 2 +- .../trackedentity/TrackedEntityAttributeLegendSetLink.java | 2 +- .../core/trackedentity/TrackedEntityInstanceFilter.java | 2 +- .../dhis/android/core/trackedentity/TrackedEntityType.java | 2 +- .../core/trackedentity/TrackedEntityTypeAttribute.java | 2 +- .../trackedentity/internal/ObjectWithUidWebResponse.java | 2 +- .../trackedentity/internal/TrackedEntityInstanceSync.java | 2 +- .../core/trackedentity/internal/TrackerBaseSync.java | 2 +- .../android/core/usecase/stock/InternalStockUseCase.java | 2 +- .../core/usecase/stock/InternalStockUseCaseTransaction.java | 2 +- .../org/hisp/dhis/android/core/user/AuthenticatedUser.java | 2 +- .../java/org/hisp/dhis/android/core/user/Authority.java | 2 +- .../src/main/java/org/hisp/dhis/android/core/user/User.java | 2 +- .../org/hisp/dhis/android/core/user/UserCredentials.java | 2 +- .../main/java/org/hisp/dhis/android/core/user/UserInfo.java | 2 +- .../dhis/android/core/user/UserOrganisationUnitLink.java | 2 +- .../dhis/android/core/visualization/CategoryDimension.java | 2 +- .../hisp/dhis/android/core/visualization/Visualization.java | 2 +- .../visualization/VisualizationCategoryDimensionLink.java | 2 +- 106 files changed, 108 insertions(+), 108 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/call/queries/internal/BaseQuery.java b/core/src/main/java/org/hisp/dhis/android/core/arch/call/queries/internal/BaseQuery.java index 9c73e4d5bb..3560c9a6e8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/call/queries/internal/BaseQuery.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/call/queries/internal/BaseQuery.java @@ -44,7 +44,7 @@ boolean isValid() { return true; } - protected static abstract class Builder { + protected abstract static class Builder { public abstract T page(int page); public abstract T pageSize(int pageSize); diff --git a/core/src/main/java/org/hisp/dhis/android/core/attribute/DataElementAttributeValueLink.java b/core/src/main/java/org/hisp/dhis/android/core/attribute/DataElementAttributeValueLink.java index 219d1770a4..c36792513f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/attribute/DataElementAttributeValueLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/attribute/DataElementAttributeValueLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder dataElement(String dataElement); diff --git a/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramAttributeValueLink.java b/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramAttributeValueLink.java index fb7a14f7bf..aa6cccc8c4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramAttributeValueLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramAttributeValueLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder program(String program); diff --git a/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramStageAttributeValueLink.java b/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramStageAttributeValueLink.java index 679d6eb034..6b73887969 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramStageAttributeValueLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/attribute/ProgramStageAttributeValueLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder programStage(String programStage); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/Category.java b/core/src/main/java/org/hisp/dhis/android/core/category/Category.java index d4b21479a0..bac8207357 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/Category.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/Category.java @@ -69,7 +69,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); public abstract Builder categoryOptions(@Nullable List categoryOptions); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryComboLink.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryComboLink.java index 29b275a5ac..a728de20c9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryComboLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryComboLink.java @@ -60,7 +60,7 @@ public static CategoryCategoryComboLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryOptionLink.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryOptionLink.java index d54053f64a..be4ac31993 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryOptionLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCategoryOptionLink.java @@ -60,7 +60,7 @@ public static CategoryCategoryOptionLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCombo.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCombo.java index 0cea339861..e20af41e85 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCombo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryCombo.java @@ -77,7 +77,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOption.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOption.java index 5d9d7439e4..6b4298d9b7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOption.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOption.java @@ -88,7 +88,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder { + public abstract static class Builder extends BaseNameableObject.Builder { public abstract Builder id(Long id); public abstract Builder startDate(@Nullable Date startDate); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionCombo.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionCombo.java index 7526563aed..dd5e289241 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionCombo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionCombo.java @@ -72,7 +72,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionComboCategoryOptionLink.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionComboCategoryOptionLink.java index 436228c2d5..8e4eb9e2e5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionComboCategoryOptionLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionComboCategoryOptionLink.java @@ -57,7 +57,7 @@ public static CategoryOptionComboCategoryOptionLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionOrganisationUnitLink.java b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionOrganisationUnitLink.java index 551ad86b02..fe8fc321b1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionOrganisationUnitLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/category/CategoryOptionOrganisationUnitLink.java @@ -60,7 +60,7 @@ public static CategoryOptionOrganisationUnitLink create(Cursor cursor) { } @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder categoryOption(String categoryOption); diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/BaseDataObject.java b/core/src/main/java/org/hisp/dhis/android/core/common/BaseDataObject.java index b713ed08f9..a761893045 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/BaseDataObject.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/BaseDataObject.java @@ -57,7 +57,7 @@ public State state() { public abstract State syncState(); @JsonPOJOBuilder(withPrefix = "") - protected static abstract class Builder extends BaseObject.Builder { + protected abstract static class Builder extends BaseObject.Builder { public abstract T syncState(@Nullable State syncState); /** diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/BaseDeletableDataObject.java b/core/src/main/java/org/hisp/dhis/android/core/common/BaseDeletableDataObject.java index eac0a783b2..1c2ae17488 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/BaseDeletableDataObject.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/BaseDeletableDataObject.java @@ -43,7 +43,7 @@ public abstract class BaseDeletableDataObject extends BaseDataObject implements public abstract Boolean deleted(); @JsonPOJOBuilder(withPrefix = "") - protected static abstract class Builder extends BaseDataObject.Builder { + protected abstract static class Builder extends BaseDataObject.Builder { public abstract T deleted(@Nullable Boolean deleted); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/BaseIdentifiableObject.java b/core/src/main/java/org/hisp/dhis/android/core/common/BaseIdentifiableObject.java index f26ba9e568..215be21c5d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/BaseIdentifiableObject.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/BaseIdentifiableObject.java @@ -107,7 +107,7 @@ public static String dateToDateStr(Date date) { } @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { @JsonProperty(UID) @JsonAlias({UUID}) // Introduced in 2.38 due to changes in userCredentials model DHIS2-12577 diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/BaseNameableObject.java b/core/src/main/java/org/hisp/dhis/android/core/common/BaseNameableObject.java index 8c404082c6..f574ddd7e3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/BaseNameableObject.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/BaseNameableObject.java @@ -55,7 +55,7 @@ public abstract class BaseNameableObject extends BaseIdentifiableObject implemen public abstract String displayDescription(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract T shortName(@Nullable String shortName); diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/BaseObject.java b/core/src/main/java/org/hisp/dhis/android/core/common/BaseObject.java index 69f1416890..a9ec7b279a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/BaseObject.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/BaseObject.java @@ -31,7 +31,7 @@ @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract") public abstract class BaseObject implements CoreObject { - public static abstract class Builder { + public abstract static class Builder { public abstract T id(Long id); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/FilterOperators.java b/core/src/main/java/org/hisp/dhis/android/core/common/FilterOperators.java index 64bff4d31f..abffbb26dc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/FilterOperators.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/FilterOperators.java @@ -103,7 +103,7 @@ public abstract class FilterOperators { public abstract DateFilterPeriod dateFilter(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract T le(String le); diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/FilterQueryCriteria.java b/core/src/main/java/org/hisp/dhis/android/core/common/FilterQueryCriteria.java index bd13010a94..ea94c18a45 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/FilterQueryCriteria.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/FilterQueryCriteria.java @@ -91,7 +91,7 @@ public abstract class FilterQueryCriteria { public abstract DateFilterPeriod lastUpdatedDate(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract T followUp(Boolean followUp); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElement.java b/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElement.java index 2c0bf39df6..630e96ccb3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElement.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElement.java @@ -120,7 +120,7 @@ public static DataElement create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder + public abstract static class Builder extends BaseNameableObject.Builder implements ObjectWithStyle.Builder { public abstract DataElement.Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElementOperand.java b/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElementOperand.java index d7e339308c..20027e33cb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElementOperand.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataelement/DataElementOperand.java @@ -85,7 +85,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { @JsonProperty(UID) public abstract Builder uid(String uid); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.java index 6ebdf297a6..a5e948f755 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.java @@ -173,7 +173,7 @@ public static DataSet create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder + public abstract static class Builder extends BaseNameableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetCompulsoryDataElementOperandLink.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetCompulsoryDataElementOperandLink.java index 4c84a24254..4b379ca6eb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetCompulsoryDataElementOperandLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetCompulsoryDataElementOperandLink.java @@ -57,7 +57,7 @@ public static DataSetCompulsoryDataElementOperandLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetElement.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetElement.java index 8e8d69adce..d27d5c1ca4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetElement.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetElement.java @@ -75,7 +75,7 @@ public static DataSetElement create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder dataSet(ObjectWithUid dataSet); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetOrganisationUnitLink.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetOrganisationUnitLink.java index 0a84a7d782..19c4f713ad 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetOrganisationUnitLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSetOrganisationUnitLink.java @@ -56,7 +56,7 @@ public static DataSetOrganisationUnitLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/Section.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/Section.java index 0fd362cb5e..8199c9dd93 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/Section.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/Section.java @@ -103,7 +103,7 @@ public static Section create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); public abstract Builder description(String description); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionDataElementLink.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionDataElementLink.java index ebc863dcc1..4dc5ab1060 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionDataElementLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionDataElementLink.java @@ -59,7 +59,7 @@ public static SectionDataElementLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionGreyedFieldsLink.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionGreyedFieldsLink.java index c6d63c2b84..a5ba88774f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionGreyedFieldsLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/SectionGreyedFieldsLink.java @@ -60,7 +60,7 @@ public static SectionGreyedFieldsLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/SectionIndicatorLink.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/SectionIndicatorLink.java index 88085c6a04..063f7d960a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/SectionIndicatorLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/SectionIndicatorLink.java @@ -57,7 +57,7 @@ public static SectionIndicatorLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/datastore/KeyValuePair.java b/core/src/main/java/org/hisp/dhis/android/core/datastore/KeyValuePair.java index 57e38b1f47..844ee743cd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/datastore/KeyValuePair.java +++ b/core/src/main/java/org/hisp/dhis/android/core/datastore/KeyValuePair.java @@ -56,7 +56,7 @@ public static Builder builder() { } @AutoValue.Builder - public static abstract class Builder { + public abstract static class Builder { public abstract Builder id(Long id); public abstract Builder key(String key); diff --git a/core/src/main/java/org/hisp/dhis/android/core/datavalue/DataValueConflict.java b/core/src/main/java/org/hisp/dhis/android/core/datavalue/DataValueConflict.java index b8a6aedaae..55a7eb59ec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/datavalue/DataValueConflict.java +++ b/core/src/main/java/org/hisp/dhis/android/core/datavalue/DataValueConflict.java @@ -93,7 +93,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder conflict(String conflict); public abstract Builder value(String value); diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSync.java b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSync.java index 4439fec072..89371436dd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSync.java +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSync.java @@ -84,7 +84,7 @@ static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - static abstract class Builder extends BaseObject.Builder { + abstract static class Builder extends BaseObject.Builder { abstract Builder dataSet(String dataSet); diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/EventDataFilter.java b/core/src/main/java/org/hisp/dhis/android/core/event/EventDataFilter.java index 8920c6a7a6..9f5076703a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/EventDataFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/EventDataFilter.java @@ -70,7 +70,7 @@ public static EventDataFilter create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterOperators.Builder { + public abstract static class Builder extends FilterOperators.Builder { public abstract Builder id(Long id); public abstract Builder eventFilter(String eventFilter); diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/EventFilter.java b/core/src/main/java/org/hisp/dhis/android/core/event/EventFilter.java index 75f6b96b20..81042a1dc9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/EventFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/EventFilter.java @@ -75,7 +75,7 @@ public static EventFilter create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/EventQueryCriteria.java b/core/src/main/java/org/hisp/dhis/android/core/event/EventQueryCriteria.java index b863d6cd4a..7d3a1eba2d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/EventQueryCriteria.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/EventQueryCriteria.java @@ -83,7 +83,7 @@ public static EventQueryCriteria create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterQueryCriteria.Builder { + public abstract static class Builder extends FilterQueryCriteria.Builder { public abstract Builder id(Long id); public abstract Builder dataFilters(List dataFilters); diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventSync.java b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventSync.java index 06283adc78..b84e436240 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventSync.java +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventSync.java @@ -55,7 +55,7 @@ static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - static abstract class Builder extends TrackerBaseSync.Builder { + abstract static class Builder extends TrackerBaseSync.Builder { abstract EventSync build(); } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/TrackerImportConflict.java b/core/src/main/java/org/hisp/dhis/android/core/imports/TrackerImportConflict.java index b82e647b1b..cf8e97eec4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/TrackerImportConflict.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/TrackerImportConflict.java @@ -95,7 +95,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder conflict(String conflict); public abstract Builder value(String value); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummaries.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummaries.java index 29c9922c53..9ae1eadb94 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummaries.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummaries.java @@ -70,7 +70,7 @@ public abstract class BaseImportSummaries { public abstract Integer ignored(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract T status(ImportStatus status); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummary.java index 411da508a8..c936c74f3c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/BaseImportSummary.java @@ -73,7 +73,7 @@ public abstract class BaseImportSummary implements ImportSummary { public abstract String description(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract T importCount(ImportCount importCount); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummaries.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummaries.java index 4d566c0e21..15948b7f70 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummaries.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummaries.java @@ -50,7 +50,7 @@ public abstract class EnrollmentImportSummaries extends BaseImportSummaries impl @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummaries.Builder { + public abstract static class Builder extends BaseImportSummaries.Builder { public abstract Builder importSummaries(List importSummaries); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummary.java index 6c4cdcc43a..12a6c77158 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EnrollmentImportSummary.java @@ -47,7 +47,7 @@ public abstract class EnrollmentImportSummary extends BaseImportSummary { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummary.Builder { + public abstract static class Builder extends BaseImportSummary.Builder { public abstract Builder events(EventImportSummaries events); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummaries.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummaries.java index 2f92b28f37..b79f668b44 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummaries.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummaries.java @@ -50,7 +50,7 @@ public abstract class EventImportSummaries extends BaseImportSummaries implement @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummaries.Builder { + public abstract static class Builder extends BaseImportSummaries.Builder { public abstract Builder importSummaries(List importSummaries); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummary.java index ed70a7a82a..94aa3efd30 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventImportSummary.java @@ -38,7 +38,7 @@ public abstract class EventImportSummary extends BaseImportSummary { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummary.Builder { + public abstract static class Builder extends BaseImportSummary.Builder { public abstract EventImportSummary build(); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventWebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventWebResponse.java index 727c3d4b2f..c92b58d1e9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventWebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/EventWebResponse.java @@ -60,7 +60,7 @@ public static EventWebResponse empty() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract Builder response(EventImportSummaries response); public abstract EventWebResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/HttpMessageResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/HttpMessageResponse.java index 5ea97e5cbe..b59bf68c93 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/HttpMessageResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/HttpMessageResponse.java @@ -38,7 +38,7 @@ public abstract class HttpMessageResponse extends WebResponse { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract HttpMessageResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteSummary.java index 30fd0f628c..9ced593a0e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteSummary.java @@ -38,7 +38,7 @@ public abstract class RelationshipDeleteSummary extends BaseImportSummary { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummary.Builder { + public abstract static class Builder extends BaseImportSummary.Builder { public abstract RelationshipDeleteSummary build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteWebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteWebResponse.java index d87efaf548..c73484a137 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteWebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipDeleteWebResponse.java @@ -60,7 +60,7 @@ public static RelationshipDeleteWebResponse empty() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract Builder response(RelationshipDeleteSummary response); public abstract RelationshipDeleteWebResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummaries.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummaries.java index 4b23aaff94..90103088dc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummaries.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummaries.java @@ -50,7 +50,7 @@ public abstract class RelationshipImportSummaries @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummaries.Builder { + public abstract static class Builder extends BaseImportSummaries.Builder { public abstract Builder importSummaries(List importSummaries); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummary.java index 083d6db127..4204d373b6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipImportSummary.java @@ -38,7 +38,7 @@ public abstract class RelationshipImportSummary extends BaseImportSummary { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummary.Builder { + public abstract static class Builder extends BaseImportSummary.Builder { public abstract RelationshipImportSummary build(); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipWebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipWebResponse.java index a7351474e2..d578211bce 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipWebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/RelationshipWebResponse.java @@ -58,7 +58,7 @@ public static RelationshipWebResponse empty() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract Builder response(RelationshipImportSummaries response); public abstract RelationshipWebResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummaries.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummaries.java index 05d93a6e11..c78b219832 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummaries.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummaries.java @@ -50,7 +50,7 @@ public abstract class TEIImportSummaries extends BaseImportSummaries implements @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummaries.Builder { + public abstract static class Builder extends BaseImportSummaries.Builder { public abstract Builder importSummaries(List importSummaries); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummary.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummary.java index 54a369acd6..de462a1c27 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummary.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIImportSummary.java @@ -47,7 +47,7 @@ public abstract class TEIImportSummary extends BaseImportSummary { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseImportSummary.Builder { + public abstract static class Builder extends BaseImportSummary.Builder { public abstract Builder enrollments(EnrollmentImportSummaries enrollments); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIWebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIWebResponse.java index e5e91b30f7..f49ccc15a8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIWebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/TEIWebResponse.java @@ -60,7 +60,7 @@ public static TEIWebResponse empty() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract Builder response(TEIImportSummaries response); public abstract TEIWebResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/WebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/WebResponse.java index 3c4390786c..92ee12d21f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/imports/internal/WebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/imports/internal/WebResponse.java @@ -47,7 +47,7 @@ public abstract class WebResponse { public abstract String message(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract T httpStatus(String httpStatus); public abstract T httpStatusCode(Integer httpStatusCode); diff --git a/core/src/main/java/org/hisp/dhis/android/core/indicator/Indicator.java b/core/src/main/java/org/hisp/dhis/android/core/indicator/Indicator.java index 915a38d8d0..393c09cc65 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/indicator/Indicator.java +++ b/core/src/main/java/org/hisp/dhis/android/core/indicator/Indicator.java @@ -103,7 +103,7 @@ public static Indicator create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder + public abstract static class Builder extends BaseNameableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/indicator/IndicatorType.java b/core/src/main/java/org/hisp/dhis/android/core/indicator/IndicatorType.java index 518432be56..8be34c124d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/indicator/IndicatorType.java +++ b/core/src/main/java/org/hisp/dhis/android/core/indicator/IndicatorType.java @@ -64,7 +64,7 @@ public static IndicatorType create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); public abstract Builder number(Boolean number); diff --git a/core/src/main/java/org/hisp/dhis/android/core/legendset/DataElementLegendSetLink.java b/core/src/main/java/org/hisp/dhis/android/core/legendset/DataElementLegendSetLink.java index dd3cde16cf..bd4318e80c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/legendset/DataElementLegendSetLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/legendset/DataElementLegendSetLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder dataElement(String dataElement); diff --git a/core/src/main/java/org/hisp/dhis/android/core/legendset/IndicatorLegendSetLink.java b/core/src/main/java/org/hisp/dhis/android/core/legendset/IndicatorLegendSetLink.java index 6d3a65fda0..e371779ac7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/legendset/IndicatorLegendSetLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/legendset/IndicatorLegendSetLink.java @@ -59,7 +59,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder indicator(String indicator); diff --git a/core/src/main/java/org/hisp/dhis/android/core/legendset/ProgramIndicatorLegendSetLink.java b/core/src/main/java/org/hisp/dhis/android/core/legendset/ProgramIndicatorLegendSetLink.java index c2a593d0e0..d7b133d92d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/legendset/ProgramIndicatorLegendSetLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/legendset/ProgramIndicatorLegendSetLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder programIndicator(String programIndicator); diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java index c24fafef36..e81a54672f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java @@ -91,7 +91,7 @@ public boolean isOffline() { } @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder url(String url); diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/ForeignKeyViolation.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/ForeignKeyViolation.java index 4dad86990c..40e20baedf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/ForeignKeyViolation.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/ForeignKeyViolation.java @@ -81,7 +81,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder fromTable(String fromTable); diff --git a/core/src/main/java/org/hisp/dhis/android/core/note/NewTrackerImporterNote.java b/core/src/main/java/org/hisp/dhis/android/core/note/NewTrackerImporterNote.java index b80dce910f..53f09361c6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/note/NewTrackerImporterNote.java +++ b/core/src/main/java/org/hisp/dhis/android/core/note/NewTrackerImporterNote.java @@ -90,7 +90,7 @@ public static NewTrackerImporterNote create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseDeletableDataObject.Builder { + public abstract static class Builder extends BaseDeletableDataObject.Builder { public abstract Builder id(Long id); @JsonProperty(NoteFields.UID) diff --git a/core/src/main/java/org/hisp/dhis/android/core/note/Note.java b/core/src/main/java/org/hisp/dhis/android/core/note/Note.java index d5d3e07dfd..01f2127828 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/note/Note.java +++ b/core/src/main/java/org/hisp/dhis/android/core/note/Note.java @@ -94,7 +94,7 @@ public static Note create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseDeletableDataObject.Builder { + public abstract static class Builder extends BaseDeletableDataObject.Builder { public abstract Builder id(Long id); @JsonProperty(NoteFields.UID) diff --git a/core/src/main/java/org/hisp/dhis/android/core/note/NoteCreateProjection.java b/core/src/main/java/org/hisp/dhis/android/core/note/NoteCreateProjection.java index 0ec8ab9ee6..8b446a990e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/note/NoteCreateProjection.java +++ b/core/src/main/java/org/hisp/dhis/android/core/note/NoteCreateProjection.java @@ -90,7 +90,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder noteType(Note.NoteType noteType); public abstract Builder event(String event); diff --git a/core/src/main/java/org/hisp/dhis/android/core/option/Option.java b/core/src/main/java/org/hisp/dhis/android/core/option/Option.java index aba82b4bf7..f5f9001210 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/option/Option.java +++ b/core/src/main/java/org/hisp/dhis/android/core/option/Option.java @@ -71,7 +71,7 @@ public static Option create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder + public abstract static class Builder extends BaseIdentifiableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroup.java b/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroup.java index 3b8455fed6..0eacbdbc5e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroup.java +++ b/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroup.java @@ -72,7 +72,7 @@ public static OptionGroup create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); public abstract Builder optionSet(@Nullable ObjectWithUid optionSet); diff --git a/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroupOptionLink.java b/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroupOptionLink.java index d7673e749a..29db4b235c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroupOptionLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/option/OptionGroupOptionLink.java @@ -57,7 +57,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder optionGroup(String optionGroup); diff --git a/core/src/main/java/org/hisp/dhis/android/core/option/OptionSet.java b/core/src/main/java/org/hisp/dhis/android/core/option/OptionSet.java index 55ce2d7847..52c03cff83 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/option/OptionSet.java +++ b/core/src/main/java/org/hisp/dhis/android/core/option/OptionSet.java @@ -68,7 +68,7 @@ public static OptionSet create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); public abstract Builder version(@Nullable Integer version); diff --git a/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitOrganisationUnitGroupLink.java b/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitOrganisationUnitGroupLink.java index a128926199..22857b2d8c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitOrganisationUnitGroupLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitOrganisationUnitGroupLink.java @@ -57,7 +57,7 @@ public static OrganisationUnitOrganisationUnitGroupLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitProgramLink.java b/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitProgramLink.java index c64c2aca79..0b03d8ed16 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitProgramLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/organisationunit/OrganisationUnitProgramLink.java @@ -56,7 +56,7 @@ public static OrganisationUnitProgramLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java index 7a59dd61ef..833bd98751 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java @@ -220,7 +220,7 @@ public static Program create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder + public abstract static class Builder extends BaseNameableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramRule.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramRule.java index 2f072bf34d..302a0034ab 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramRule.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramRule.java @@ -85,7 +85,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramSectionAttributeLink.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramSectionAttributeLink.java index c688553157..9186063073 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramSectionAttributeLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramSectionAttributeLink.java @@ -59,7 +59,7 @@ public static ProgramSectionAttributeLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java index 5fa3b9b3d9..827a87ecbb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java @@ -210,7 +210,7 @@ public static ProgramStage create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder + public abstract static class Builder extends BaseIdentifiableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionDataElementLink.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionDataElementLink.java index 723b15f7a3..12a96df842 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionDataElementLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionDataElementLink.java @@ -59,7 +59,7 @@ public static ProgramStageSectionDataElementLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionProgramIndicatorLink.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionProgramIndicatorLink.java index d22a3f9dfe..1f00f2214e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionProgramIndicatorLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageSectionProgramIndicatorLink.java @@ -57,7 +57,7 @@ public static ProgramStageSectionProgramIndicatorLink create(Cursor cursor) { @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageQueryCriteria.java b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageQueryCriteria.java index c70e874e4a..eceaeaf342 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageQueryCriteria.java +++ b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageQueryCriteria.java @@ -143,7 +143,7 @@ public static ProgramStageQueryCriteria create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingList.java b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingList.java index a8e0d436b2..bc3759b83d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingList.java +++ b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingList.java @@ -77,7 +77,7 @@ public static ProgramStageWorkingList create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListAttributeValueFilter.java b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListAttributeValueFilter.java index bad4a12604..4f4f6cbbf9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListAttributeValueFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListAttributeValueFilter.java @@ -84,7 +84,7 @@ public static ProgramStageWorkingListAttributeValueFilter create(Cursor cursor) @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterOperators.Builder { + public abstract static class Builder extends FilterOperators.Builder { public abstract Builder id(Long id); public abstract Builder programStageWorkingList(String programStageWorkingList); diff --git a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListEventDataFilter.java b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListEventDataFilter.java index 09c581675c..c59e6b9c3d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListEventDataFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/programstageworkinglist/ProgramStageWorkingListEventDataFilter.java @@ -70,7 +70,7 @@ public static ProgramStageWorkingListEventDataFilter create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterOperators.Builder { + public abstract static class Builder extends FilterOperators.Builder { public abstract Builder id(Long id); public abstract Builder programStageWorkingList(String programStageWorkingList); diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java index 5daac24b4e..3aead715e5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipConstraint.java @@ -91,7 +91,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder relationshipType(ObjectWithUid relationshipType); diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipType.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipType.java index 28b0b745c1..2c1b7733f3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipType.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipType.java @@ -99,7 +99,7 @@ public static RelationshipType create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); abstract Builder bIsToA(String bIsToA); diff --git a/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSMetadataId.java b/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSMetadataId.java index 0ce5747c22..79d4bf82ec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSMetadataId.java +++ b/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSMetadataId.java @@ -60,7 +60,7 @@ public static SMSMetadataId create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder { + public abstract static class Builder { public abstract Builder id(Long id); public abstract Builder type(SMSConsts.MetadataType smsMetadataIdType); diff --git a/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSOngoingSubmission.java b/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSOngoingSubmission.java index d64198c046..392fc48c97 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSOngoingSubmission.java +++ b/core/src/main/java/org/hisp/dhis/android/core/sms/data/localdbrepository/internal/SMSOngoingSubmission.java @@ -60,7 +60,7 @@ public static SMSOngoingSubmission create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder { + public abstract static class Builder { public abstract Builder id(Long id); public abstract Builder submissionId(Integer submissionId); diff --git a/core/src/main/java/org/hisp/dhis/android/core/sms/data/webapirepository/internal/MetadataResponse.java b/core/src/main/java/org/hisp/dhis/android/core/sms/data/webapirepository/internal/MetadataResponse.java index d7c636121a..b76cf37510 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/sms/data/webapirepository/internal/MetadataResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/sms/data/webapirepository/internal/MetadataResponse.java @@ -70,7 +70,7 @@ public abstract static class MetadataSystemInfo { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract MetadataSystemInfo.Builder date(Date date); @@ -85,7 +85,7 @@ public abstract static class MetadataId { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract MetadataId.Builder id(String id); @@ -95,7 +95,7 @@ public static abstract class Builder { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract MetadataResponse.Builder system(MetadataSystemInfo systemInfo); diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SystemInfo.java b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SystemInfo.java index d552c09698..49a1ade426 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SystemInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SystemInfo.java @@ -76,7 +76,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder serverDate(Date serverDate); public abstract Builder dateFormat(String dateFormat); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/AttributeValueFilter.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/AttributeValueFilter.java index cda74fa9a9..741082f4fc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/AttributeValueFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/AttributeValueFilter.java @@ -84,7 +84,7 @@ public static AttributeValueFilter create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterOperators.Builder { + public abstract static class Builder extends FilterOperators.Builder { public abstract Builder id(Long id); public abstract Builder trackedEntityInstanceFilter(String trackedEntityInstanceFilter); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/EntityQueryCriteria.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/EntityQueryCriteria.java index 9a8ba117e2..d8b0602ec6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/EntityQueryCriteria.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/EntityQueryCriteria.java @@ -98,7 +98,7 @@ public static EntityQueryCriteria create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends FilterQueryCriteria.Builder { + public abstract static class Builder extends FilterQueryCriteria.Builder { public abstract Builder id(Long id); public abstract Builder programStage(String programStage); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityAttributeLegendSetLink.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityAttributeLegendSetLink.java index 49edb1136b..87189f8813 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityAttributeLegendSetLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityAttributeLegendSetLink.java @@ -60,7 +60,7 @@ public static Builder builder() { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder trackedEntityAttribute(String trackedEntityAttribute); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityInstanceFilter.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityInstanceFilter.java index d68c094bd7..61155a8a9f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityInstanceFilter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityInstanceFilter.java @@ -125,7 +125,7 @@ public static TrackedEntityInstanceFilter create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder + public abstract static class Builder extends BaseIdentifiableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityType.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityType.java index cc6e52848d..127e1f3250 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityType.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityType.java @@ -82,7 +82,7 @@ public static TrackedEntityType create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseNameableObject.Builder + public abstract static class Builder extends BaseNameableObject.Builder implements ObjectWithStyle.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityTypeAttribute.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityTypeAttribute.java index b4410f69eb..c6c04446b5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityTypeAttribute.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/TrackedEntityTypeAttribute.java @@ -82,7 +82,7 @@ public static TrackedEntityTypeAttribute create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/ObjectWithUidWebResponse.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/ObjectWithUidWebResponse.java index 03723fc70e..c6e05c48f2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/ObjectWithUidWebResponse.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/ObjectWithUidWebResponse.java @@ -50,7 +50,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends WebResponse.Builder { + public abstract static class Builder extends WebResponse.Builder { public abstract Builder response(ObjectWithUid response); public abstract ObjectWithUidWebResponse build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceSync.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceSync.java index 6d66997bb9..490a183fee 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceSync.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceSync.java @@ -53,7 +53,7 @@ static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - static abstract class Builder extends TrackerBaseSync.Builder { + abstract static class Builder extends TrackerBaseSync.Builder { abstract TrackedEntityInstanceSync build(); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackerBaseSync.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackerBaseSync.java index fc97d7b477..03768cf895 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackerBaseSync.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackerBaseSync.java @@ -55,7 +55,7 @@ public abstract class TrackerBaseSync extends BaseObject { public abstract Date lastUpdated(); @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract T program(String program); diff --git a/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCase.java b/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCase.java index 091f0fc387..b392ec3234 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCase.java +++ b/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCase.java @@ -93,7 +93,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); @JsonProperty("programUid") diff --git a/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCaseTransaction.java b/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCaseTransaction.java index 604f0657aa..ecb9d2d96c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCaseTransaction.java +++ b/core/src/main/java/org/hisp/dhis/android/core/usecase/stock/InternalStockUseCaseTransaction.java @@ -85,7 +85,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { @JsonProperty(UID) public abstract Builder programUid(String programUid); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/AuthenticatedUser.java b/core/src/main/java/org/hisp/dhis/android/core/user/AuthenticatedUser.java index 17891df69e..f137c0151b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/AuthenticatedUser.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/AuthenticatedUser.java @@ -58,7 +58,7 @@ public static AuthenticatedUser create(Cursor cursor) { @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/Authority.java b/core/src/main/java/org/hisp/dhis/android/core/user/Authority.java index 2d03d11c57..5ff85365aa 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/Authority.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/Authority.java @@ -61,7 +61,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/User.java b/core/src/main/java/org/hisp/dhis/android/core/user/User.java index ad47f00aee..9aeb3d0caf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/User.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/User.java @@ -124,7 +124,7 @@ public static User create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserCredentials.java b/core/src/main/java/org/hisp/dhis/android/core/user/UserCredentials.java index a4f4b1b274..251167611e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserCredentials.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserCredentials.java @@ -65,7 +65,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder username(String username); public abstract Builder name(String username); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserInfo.java b/core/src/main/java/org/hisp/dhis/android/core/user/UserInfo.java index 2d357384d0..953b1d4d22 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserInfo.java @@ -66,7 +66,7 @@ public static UserInfo create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder uid(String uid); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserOrganisationUnitLink.java b/core/src/main/java/org/hisp/dhis/android/core/user/UserOrganisationUnitLink.java index 4a9964580e..dfda6f8b6b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserOrganisationUnitLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserOrganisationUnitLink.java @@ -59,7 +59,7 @@ public static UserOrganisationUnitLink create(Cursor cursor) { public abstract Builder toBuilder(); @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/CategoryDimension.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/CategoryDimension.java index ce758338b8..5c8101d46b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/CategoryDimension.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/CategoryDimension.java @@ -59,7 +59,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder { + public abstract static class Builder { public abstract Builder category(ObjectWithUid category); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/Visualization.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/Visualization.java index 6b43b1a44e..3879335b9e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/Visualization.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/Visualization.java @@ -197,7 +197,7 @@ public static Visualization create(Cursor cursor) { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public static abstract class Builder extends BaseIdentifiableObject.Builder { + public abstract static class Builder extends BaseIdentifiableObject.Builder { public abstract Builder id(Long id); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationCategoryDimensionLink.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationCategoryDimensionLink.java index 91f521c19e..c34c39cc32 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationCategoryDimensionLink.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationCategoryDimensionLink.java @@ -61,7 +61,7 @@ public static VisualizationCategoryDimensionLink create(Cursor cursor) { } @AutoValue.Builder - public static abstract class Builder extends BaseObject.Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder visualization(String visualization); From 04b9db95c1aff99c4cb5fd755e6679ee5bee3ab1 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 11 Jan 2024 11:22:49 +0100 Subject: [PATCH 021/222] [ANDROSDK-1693] Refactor related item download to avoid duplications --- ...tyInstanceCallBaseMockIntegrationShould.kt | 3 +- ...ityInstanceCallNewMockIntegrationShould.kt | 3 +- ...ityInstanceCallOldMockIntegrationShould.kt | 1 + ...nloadCallEnqueableMockIntegrationShould.kt | 2 - .../core/arch/d2/internal/JavaDIClasses.kt | 2 - .../internal/IdentifiableDataHandler.kt | 2 +- .../internal/IdentifiableDataHandlerImpl.kt | 1 - .../RelationshipDHISVersionManager.kt | 26 +-- ...lationshipDownloadAndPersistCallFactory.kt | 89 +++++++---- .../internal/RelationshipHandler.kt | 6 - .../RelationshipItemElementStoreSelector.kt | 2 + ...elationshipItemElementStoreSelectorImpl.kt | 22 ++- .../internal/RelationshipItemRelative.kt | 5 +- .../internal/RelationshipItemRelatives.kt | 2 +- .../NewTrackedEntityEndpointCallFactory.kt | 3 +- ...racker_importer_tracked_entity_single.json | 150 ------------------ .../TrackedEntityInstanceHandlerShould.kt | 1 - 17 files changed, 102 insertions(+), 218 deletions(-) delete mode 100644 core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entity_single.json diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallBaseMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallBaseMockIntegrationShould.kt index c5285d2806..b85fcc49c3 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallBaseMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallBaseMockIntegrationShould.kt @@ -62,6 +62,7 @@ abstract class TrackedEntityInstanceCallBaseMockIntegrationShould : BaseMockInte abstract val teiSingleFile: String abstract val teiWithRemovedDataFile: String abstract val teiWithRelationshipFile: String + abstract val teiAsRelationshipFile: String private lateinit var initSyncParams: SynchronizationSettings private val syncStore = SynchronizationSettingStoreImpl(databaseAdapter) @@ -156,7 +157,7 @@ abstract class TrackedEntityInstanceCallBaseMockIntegrationShould : BaseMockInte fun downloadAndPersistRelatedItems() { dhis2MockServer.enqueueSystemInfoResponse() dhis2MockServer.enqueueMockResponse(teiWithRelationshipFile) - dhis2MockServer.enqueueMockResponse(teiCollectionFile) + dhis2MockServer.enqueueMockResponse(teiAsRelationshipFile) d2.trackedEntityModule().trackedEntityInstanceDownloader() .byProgramUid("IpHINAT79UW") diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallNewMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallNewMockIntegrationShould.kt index 5ed9880fac..51854863a2 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallNewMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallNewMockIntegrationShould.kt @@ -44,10 +44,11 @@ class TrackedEntityInstanceCallNewMockIntegrationShould : TrackedEntityInstanceC override val exporterVersion = TrackerExporterVersion.V2 override val teiFile = "trackedentity/new_tracker_importer_tracked_entity.json" override val teiCollectionFile = "trackedentity/new_tracker_importer_tracked_entity_collection.json" - override val teiSingleFile = "trackedentity/new_tracker_importer_tracked_entity_single.json" + override val teiSingleFile = teiFile override val teiWithRemovedDataFile = "trackedentity/new_tracker_importer_tracked_entity_with_removed_data_single.json" override val teiWithRelationshipFile = "trackedentity/new_tracker_importer_tracked_entity_with_relationship.json" + override val teiAsRelationshipFile = teiFile override fun parseTrackedEntityInstance(file: String): TrackedEntityInstance { val expectedEventsResponseJson = ResourcesFileReader().getStringFromFile(file) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallOldMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallOldMockIntegrationShould.kt index e850187165..e7912467f9 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallOldMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallOldMockIntegrationShould.kt @@ -46,6 +46,7 @@ class TrackedEntityInstanceCallOldMockIntegrationShould : TrackedEntityInstanceC override val teiSingleFile = "trackedentity/tracked_entity_instance_single.json" override val teiWithRemovedDataFile = "trackedentity/tracked_entity_instance_with_removed_data_single.json" override val teiWithRelationshipFile = "trackedentity/tracked_entity_instances_with_relationship.json" + override val teiAsRelationshipFile = teiCollectionFile override fun parseTrackedEntityInstance(file: String): TrackedEntityInstance { val expectedEventsResponseJson = ResourcesFileReader().getStringFromFile(file) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloadCallEnqueableMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloadCallEnqueableMockIntegrationShould.kt index 3d5868147c..38fdab27d7 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloadCallEnqueableMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloadCallEnqueableMockIntegrationShould.kt @@ -46,12 +46,10 @@ class TrackedEntityInstanceDownloadCallEnqueableMockIntegrationShould : BaseMock @Test fun should_continue_on_page_error() { val programTeis = "trackedentity/new_tracker_importer_tracked_entities.json" - val relationshipTei = "trackedentity/new_tracker_importer_tracked_entity_collection.json" dhis2MockServer.enqueueSystemInfoResponse() dhis2MockServer.enqueueMockResponse(403) dhis2MockServer.enqueueMockResponse(programTeis) - dhis2MockServer.enqueueMockResponse(relationshipTei) d2.trackedEntityModule().trackedEntityInstanceDownloader().blockingDownload() diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt index 67f6c6d7cf..951ca141cb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt @@ -38,7 +38,6 @@ import org.hisp.dhis.android.core.maintenance.internal.ForeignKeyCleanerImpl import org.hisp.dhis.android.core.note.internal.NoteUniquenessManager import org.hisp.dhis.android.core.period.internal.PeriodHelper import org.hisp.dhis.android.core.period.internal.PeriodParser -import org.hisp.dhis.android.core.relationship.internal.RelationshipDHISVersionManager import org.hisp.dhis.android.core.sms.data.localdbrepository.internal.DataSetsStore import org.hisp.dhis.android.core.sms.data.localdbrepository.internal.FileResourceCleaner import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceService @@ -56,6 +55,5 @@ internal val javaDIClasses = module { single { DatabaseAdapterFactory(get(), get()) } single { DatabaseExport(get(), get(), get()) } single { D2CallExecutor(get(), get()) } - single { RelationshipDHISVersionManager(get()) } single { DataSetsStore(get(), get(), get(), get()) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandler.kt index 3c546a7838..b82fbf8601 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandler.kt @@ -31,7 +31,7 @@ import org.hisp.dhis.android.core.common.DeletableDataObject import org.hisp.dhis.android.core.common.ObjectWithUidInterface import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelatives -interface IdentifiableDataHandler where O : DeletableDataObject, O : ObjectWithUidInterface { +internal interface IdentifiableDataHandler where O : DeletableDataObject, O : ObjectWithUidInterface { @JvmSuppressWildcards fun handleMany( oCollection: Collection?, diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandlerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandlerImpl.kt index 1ae357f55a..ee3073f305 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandlerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/IdentifiableDataHandlerImpl.kt @@ -141,7 +141,6 @@ internal abstract class IdentifiableDataHandlerImpl( ownedRelationships, parent.uid(), relatives, - relationshipHandler, ) } relationshipHandler.handleMany( diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt index 08a673ac86..9cd60baecf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDHISVersionManager.kt @@ -35,9 +35,11 @@ import org.hisp.dhis.android.core.relationship.RelationshipItem import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.ENROLLMENT import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.EVENT import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo.Columns.TRACKED_ENTITY_INSTANCE +import org.koin.core.annotation.Singleton +@Singleton internal class RelationshipDHISVersionManager( - private val relationshipTypeStore: RelationshipTypeStore + private val relationshipTypeStore: RelationshipTypeStore, ) { fun getOwnedRelationships(relationships: Collection, elementUid: String): List { return relationships.filter { relationship -> @@ -53,17 +55,19 @@ internal class RelationshipDHISVersionManager( } private fun getRelatedRelationshipItem(baseRelationship: BaseRelationship, parentUid: String): RelationshipItem? { - val fromUid = baseRelationship.from()?.elementUid() ?: return null - val toUid = baseRelationship.to()?.elementUid() ?: return null + val fromUid = baseRelationship.from()?.elementUid() + val toUid = baseRelationship.to()?.elementUid() - val itemBuilder = - if (parentUid == fromUid) { + val itemBuilder = when { + parentUid == fromUid -> baseRelationship.to()?.toBuilder() ?.relationshipItemType(RelationshipConstraintType.TO) - } else { + parentUid == toUid -> baseRelationship.from()?.toBuilder() ?.relationshipItemType(RelationshipConstraintType.FROM) - } + else -> + null + } return itemBuilder ?.relationship(ObjectWithUid.create(baseRelationship.uid())) @@ -74,15 +78,15 @@ internal class RelationshipDHISVersionManager( relationships: Collection, parentUid: String, relatives: RelationshipItemRelatives, - relationshipHandler: RelationshipHandler ) { for (relationship in relationships) { val item = getRelatedRelationshipItem(relationship, parentUid) - if (item != null && !relationshipHandler.doesRelationshipItemExist(item)) { + if (item != null && relationship.relationshipType() != null && item.relationshipItemType() != null) { val relationshipItem = RelationshipItemRelative( itemUid = item.elementUid(), - relationshipTypeUid = relationship.relationshipType() ?: continue, - itemType = item.relationshipItemType() ?: continue + itemType = item.elementType(), + relationshipTypeUid = relationship.relationshipType()!!, + constraintType = item.relationshipItemType()!!, ) when (item.elementType()) { TRACKED_ENTITY_INSTANCE -> relatives.addTrackedEntityInstance(relationshipItem) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt index 09da6308c1..9044b42a10 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipDownloadAndPersistCallFactory.kt @@ -32,7 +32,12 @@ import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.internal.EnrollmentPersistenceCallFactory import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.internal.EventPersistenceCallFactory -import org.hisp.dhis.android.core.relationship.* +import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.relationship.RelationshipItem +import org.hisp.dhis.android.core.relationship.RelationshipItemEnrollment +import org.hisp.dhis.android.core.relationship.RelationshipItemEvent +import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo +import org.hisp.dhis.android.core.relationship.RelationshipItemTrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityInstancePersistenceCallFactory import org.hisp.dhis.android.core.trackedentity.internal.TrackerParentCallFactory @@ -46,6 +51,7 @@ internal class RelationshipDownloadAndPersistCallFactory( private val enrollmentPersistenceCallFactory: EnrollmentPersistenceCallFactory, private val eventPersistenceCallFactory: EventPersistenceCallFactory, private val coroutineAPICallExecutor: CoroutineAPICallExecutor, + private val relationshipItemStoreSelector: RelationshipItemElementStoreSelector, ) { suspend fun downloadAndPersist(relatives: RelationshipItemRelatives) { downloadRelativeEvents(relatives) @@ -58,19 +64,25 @@ internal class RelationshipDownloadAndPersistCallFactory( val failedEvents: MutableList = mutableListOf() for (item in relatives.getRelativeEvents()) { - coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getEventCall().getRelationshipEntityCall(item) - }.fold( - onSuccess = { eventPayload -> - events.addAll(eventPayload.items()) - eventPayload.items().mapNotNull { it.enrollment() }.forEach { enrollment -> - val relativeEnrollment = - RelationshipItemRelative(enrollment, item.relationshipTypeUid, item.itemType) - relatives.addEnrollment(relativeEnrollment) - } - }, - onFailure = { failedEvents.add(item.itemUid) }, - ) + if (itemDoesNotExist(item)) { + coroutineAPICallExecutor.wrap(storeError = true) { + trackerParentCallFactory.getEventCall().getRelationshipEntityCall(item) + }.fold( + onSuccess = { eventPayload -> + events.addAll(eventPayload.items()) + eventPayload.items().mapNotNull { it.enrollment() }.forEach { enrollment -> + val relativeEnrollment = RelationshipItemRelative( + itemUid = enrollment, + itemType = RelationshipItemTableInfo.Columns.ENROLLMENT, + relationshipTypeUid = item.relationshipTypeUid, + constraintType = item.constraintType, + ) + relatives.addEnrollment(relativeEnrollment) + } + }, + onFailure = { failedEvents.add(item.itemUid) }, + ) + } } eventPersistenceCallFactory.persistAsRelationships(events) @@ -83,18 +95,25 @@ internal class RelationshipDownloadAndPersistCallFactory( val failedEnrollments: MutableList = mutableListOf() for (item in relatives.getRelativeEnrollments()) { - coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getEnrollmentCall().getRelationshipEntityCall(item) - }.fold( - onSuccess = { enrollment -> - enrollments.add(enrollment) - enrollment.trackedEntityInstance()?.let { tei -> - val relativeTei = RelationshipItemRelative(tei, item.relationshipTypeUid, item.itemType) - relatives.addTrackedEntityInstance(relativeTei) - } - }, - onFailure = { failedEnrollments.add(item.itemUid) }, - ) + if (itemDoesNotExist(item)) { + coroutineAPICallExecutor.wrap(storeError = true) { + trackerParentCallFactory.getEnrollmentCall().getRelationshipEntityCall(item) + }.fold( + onSuccess = { enrollment -> + enrollments.add(enrollment) + enrollment.trackedEntityInstance()?.let { tei -> + val relativeTei = RelationshipItemRelative( + itemUid = tei, + itemType = RelationshipItemTableInfo.Columns.TRACKED_ENTITY_INSTANCE, + relationshipTypeUid = item.relationshipTypeUid, + constraintType = item.constraintType, + ) + relatives.addTrackedEntityInstance(relativeTei) + } + }, + onFailure = { failedEnrollments.add(item.itemUid) }, + ) + } } enrollmentPersistenceCallFactory.persistAsRelationships(enrollments).blockingAwait() @@ -107,12 +126,14 @@ internal class RelationshipDownloadAndPersistCallFactory( val failedTeis: MutableList = mutableListOf() for (item in relatives.getRelativeTrackedEntityInstances()) { - coroutineAPICallExecutor.wrap(storeError = true) { - trackerParentCallFactory.getTrackedEntityCall().getRelationshipEntityCall(item) - }.fold( - onSuccess = { teiPayload -> teis.addAll(teiPayload.items()) }, - onFailure = { failedTeis.add(item.itemUid) }, - ) + if (itemDoesNotExist(item)) { + coroutineAPICallExecutor.wrap(storeError = true) { + trackerParentCallFactory.getTrackedEntityCall().getRelationshipEntityCall(item) + }.fold( + onSuccess = { teiPayload -> teis.addAll(teiPayload.items()) }, + onFailure = { failedTeis.add(item.itemUid) }, + ) + } } teiPersistenceCallFactory.persistRelationships(teis) @@ -146,4 +167,8 @@ internal class RelationshipDownloadAndPersistCallFactory( relationshipStore.deleteById(r) } } + + private fun itemDoesNotExist(item: RelationshipItemRelative): Boolean { + return !relationshipItemStoreSelector.getElementStore(item).exists(item.itemUid) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandler.kt index d5db5e2796..d2a5068137 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandler.kt @@ -32,7 +32,6 @@ import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.relationship.Relationship import org.hisp.dhis.android.core.relationship.RelationshipConstraintType -import org.hisp.dhis.android.core.relationship.RelationshipItem import org.koin.core.annotation.Singleton @Singleton @@ -40,7 +39,6 @@ internal class RelationshipHandler( relationshipStore: RelationshipStore, private val relationshipItemStore: RelationshipItemStore, private val relationshipItemHandler: RelationshipItemHandler, - private val storeSelector: RelationshipItemElementStoreSelector, ) : IdentifiableHandlerImpl(relationshipStore) { override fun afterObjectHandled(o: Relationship, action: HandleAction) { @@ -60,10 +58,6 @@ internal class RelationshipHandler( return getExistingRelationshipUid(relationship) != null } - fun doesRelationshipItemExist(item: RelationshipItem): Boolean { - return storeSelector.getElementStore(item).exists(item.elementUid()) - } - fun deleteLinkedRelationships(entityUid: String) { relationshipItemStore.getByEntityUid(entityUid) .mapNotNull { it.relationship()?.uid() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelector.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelector.kt index 2e1ddcf668..1dbec40ef8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelector.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelector.kt @@ -32,4 +32,6 @@ import org.hisp.dhis.android.core.relationship.RelationshipItem internal interface RelationshipItemElementStoreSelector { fun getElementStore(item: RelationshipItem?): StoreWithState<*> + + fun getElementStore(item: RelationshipItemRelative): StoreWithState<*> } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelectorImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelectorImpl.kt index 92ab65d988..44c7205a70 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelectorImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemElementStoreSelectorImpl.kt @@ -31,6 +31,7 @@ import org.hisp.dhis.android.core.arch.db.stores.internal.StoreWithState import org.hisp.dhis.android.core.enrollment.internal.EnrollmentStore import org.hisp.dhis.android.core.event.internal.EventStore import org.hisp.dhis.android.core.relationship.RelationshipItem +import org.hisp.dhis.android.core.relationship.RelationshipItemTableInfo import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityInstanceStore import org.koin.core.annotation.Singleton @@ -41,12 +42,21 @@ internal class RelationshipItemElementStoreSelectorImpl( private val eventStore: EventStore, ) : RelationshipItemElementStoreSelector { override fun getElementStore(item: RelationshipItem?): StoreWithState<*> { - return if (item!!.hasTrackedEntityInstance()) { - trackedEntityInstanceStore - } else if (item.hasEnrollment()) { - enrollmentStore - } else { - eventStore + return getElementStoreByElementType(item?.elementType()) + } + + override fun getElementStore(item: RelationshipItemRelative): StoreWithState<*> { + return getElementStoreByElementType(item.itemType) + } + + private fun getElementStoreByElementType(elementType: String?): StoreWithState<*> { + return when (elementType) { + RelationshipItemTableInfo.Columns.TRACKED_ENTITY_INSTANCE -> + trackedEntityInstanceStore + RelationshipItemTableInfo.Columns.ENROLLMENT -> + enrollmentStore + else -> + eventStore } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt index c73d6d0b5d..076b0958bb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelative.kt @@ -29,8 +29,9 @@ package org.hisp.dhis.android.core.relationship.internal import org.hisp.dhis.android.core.relationship.RelationshipConstraintType -data class RelationshipItemRelative( +internal data class RelationshipItemRelative( val itemUid: String, + val itemType: String, val relationshipTypeUid: String, - val itemType: RelationshipConstraintType + val constraintType: RelationshipConstraintType, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt index 28961229da..a57aa985bf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipItemRelatives.kt @@ -27,7 +27,7 @@ */ package org.hisp.dhis.android.core.relationship.internal -class RelationshipItemRelatives { +internal class RelationshipItemRelatives { private val relativeTrackedEntityInstanceItems: MutableSet = mutableSetOf() private val relativeEnrollmentItems: MutableSet = mutableSetOf() private val relativeEventItems: MutableSet = mutableSetOf() diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index 142c4c4659..d4372e70b3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -52,6 +52,7 @@ import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton @Singleton +@Suppress("TooManyFunctions") internal class NewTrackedEntityEndpointCallFactory( private val trackedExporterService: TrackerExporterService, private val coroutineAPICallExecutor: CoroutineAPICallExecutor, @@ -248,7 +249,7 @@ internal class NewTrackedEntityEndpointCallFactory( .uid(item.relationshipTypeUid) .blockingGet() - val constraint = when(item.itemType) { + val constraint = when (item.constraintType) { RelationshipConstraintType.FROM -> relationshipType?.fromConstraint() RelationshipConstraintType.TO -> relationshipType?.toConstraint() } diff --git a/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entity_single.json b/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entity_single.json deleted file mode 100644 index c38bc74823..0000000000 --- a/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entity_single.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "updatedAt": "2015-10-15T11:32:27.242", - "createdAt": "2014-06-06T20:44:21.375", - "trackedEntityType": "nEenWmSyUEp", - "orgUnit": "DiszpKrYNg8", - "trackedEntity": "PgmUFEQYZdt", - "geometry": { - "type": "Point", - "coordinates": [ - 9.0, - 9.0 - ] - }, - "deleted": false, - "relationships": [], - "attributes": [ - { - "updatedAt": "2017-12-12T07:35:12.904", - "createdAt": "2017-12-12T07:35:11.366", - "attribute": "cejWyOfXge6", - "value": "1972-11-08" - } - ], - "enrollments": [ - { - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "p6xHz0sbDlx", - "trackedEntity": "PgmUFEQYZdt", - "enrolledAt": "2017-12-12T01:00:00.000", - "followUp": false, - "deleted": false, - "occurredAt": "2017-12-12T07:33:52.993", - "status": "ACTIVE", - "events": [ - { - "programStage": "dBwrot7S420", - "scheduledAt": "2017-12-12T00:00:00.000", - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "p6xHz0sbDlx", - "event": "yVTi4EG84wp", - "occurredAt": "2017-12-12T00:00:00.000", - "status": "SCHEDULE", - "createdAt": "2017-12-12T07:33:53.613", - "updatedAt": "2017-12-12T07:35:11.917", - "deleted": false, - "dataValues": [ - { - "updatedAt": "2017-12-12T07:35:12.167", - "storedBy": "android", - "createdAt": "2017-12-12T07:35:12.166", - "dataElement": "sWoqcoByYmD", - "value": "false", - "providedElsewhere": false - } - ] - } - ], - "notes": [] - }, - { - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "WKPoiZxZxNG", - "trackedEntity": "PgmUFEQYZdt", - "enrolledAt": "2017-01-20T00:00:00.000", - "followUp": false, - "deleted": false, - "occurredAt": "2017-01-20T00:00:00.000", - "status": "CANCELLED", - "events": [ - { - "attributeOptionCombo": "bRowv6yZOF2", - "programStage": "dBwrot7S420", - "scheduledAt": "2017-12-12T07:30:12.535", - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "WKPoiZxZxNG", - "event": "AUEQ24HuW4H", - "occurredAt": "2017-01-20T00:00:00.000", - "status": "ACTIVE", - "createdAt": "2017-01-20T12:14:46.389", - "updatedAt": "2017-12-12T07:30:12.536", - "deleted": false, - "dataValues": [ - { - "updatedAt": "2017-12-12T07:30:12.541", - "storedBy": "android", - "createdAt": "2017-12-12T07:30:12.541", - "dataElement": "sWoqcoByYmD", - "value": "medication 1", - "providedElsewhere": false - } - ] - }, - { - "attributeOptionCombo": "bRowv6yZOF2", - "programStage": "dBwrot7S420", - "scheduledAt": "2017-12-12T07:30:41.755", - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "WKPoiZxZxNG", - "event": "LN9rXOMdkDM", - "occurredAt": "2017-12-12T00:00:00.000", - "status": "ACTIVE", - "createdAt": "2017-12-12T07:30:16.658", - "updatedAt": "2017-12-12T07:30:41.756", - "deleted": false, - "dataValues": [ - { - "updatedAt": "2017-12-12T07:30:41.762", - "storedBy": "android", - "createdAt": "2017-12-12T07:30:41.762", - "dataElement": "sWoqcoByYmD", - "value": "Sufficiently immunized", - "providedElsewhere": false - } - ] - }, - { - "attributeOptionCombo": "bRowv6yZOF2", - "programStage": "dBwrot7S420", - "scheduledAt": "2017-12-12T07:32:16.006", - "orgUnit": "DiszpKrYNg8", - "program": "lxAQ7Zs9VYR", - "enrollment": "WKPoiZxZxNG", - "event": "S4OBgYm4bOP", - "occurredAt": "2017-12-12T00:00:00.000", - "status": "COMPLETED", - "createdAt": "2017-12-12T07:31:30.874", - "completedAt": "2017-12-12T00:00:00.000", - "updatedAt": "2017-12-12T07:32:16.012", - "deleted": false, - "dataValues": [ - { - "updatedAt": "2017-12-12T07:32:16.046", - "storedBy": "android", - "createdAt": "2017-12-12T07:31:58.340", - "dataElement": "sWoqcoByYmD", - "value": "false", - "providedElsewhere": false - } - ] - } - ], - "notes": [] - } - ] -} diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt index 9782dbee60..381edfab6a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceHandlerShould.kt @@ -196,7 +196,6 @@ class TrackedEntityInstanceHandlerShould { listOf(relationship), TEI_UID, relatives, - relationshipHandler, ) } From bb5c2d3df2e59a0b0776b13f48acc50eaa75fb25 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 11 Jan 2024 11:57:48 +0100 Subject: [PATCH 022/222] [ANDROSDK-1693] Adapt unit test --- .../core/relationship/internal/RelationshipHandlerShould.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandlerShould.kt index c10b3333ad..d67448cdf1 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/relationship/internal/RelationshipHandlerShould.kt @@ -48,8 +48,6 @@ class RelationshipHandlerShould { private val relationshipItemHandler: RelationshipItemHandler = mock() - private val storeSelector: RelationshipItemElementStoreSelector = mock() - private val itemElementStore: StoreWithState<*> = mock() private val NEW_UID = "new-uid" @@ -71,9 +69,7 @@ class RelationshipHandlerShould { relationshipStore, relationshipItemStore, relationshipItemHandler, - storeSelector, ) - whenever(storeSelector.getElementStore(any())).thenReturn(itemElementStore) whenever(itemElementStore.exists(RelationshipSamples.FROM_UID)).thenReturn(true) whenever(itemElementStore.exists(RelationshipSamples.TO_UID)).thenReturn(true) whenever(itemElementStore.exists(TEI_3_UID)).thenReturn(true) From e0663a3c3d9227acfd5ee910bb2a24a83a44cf76 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 12 Jan 2024 12:15:31 +0100 Subject: [PATCH 023/222] [ANDROSDK-1696] Remove query and attributes from TEI search --- .../NewTrackedEntityEndpointCallFactory.kt | 3 --- .../internal/TrackedEntityInstanceService.kt | 2 -- .../TrackedEntityInstanceQueryCallFactory.kt | 1 - .../TrackedEntityInstanceQueryOnline.kt | 1 - .../TrackedEntityInstanceQueryOnlineHelper.kt | 3 --- .../search/TrackedEntitySearchOperators.kt | 23 ++++--------------- .../exporter/TrackerExporterService.kt | 2 -- .../TrackedEntityInstanceQueryCallShould.kt | 3 --- ...edEntityInstanceQueryOnlineHelperShould.kt | 13 ----------- 9 files changed, 5 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index d4372e70b3..b0fe30d842 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -195,7 +195,6 @@ internal class NewTrackedEntityEndpointCallFactory( eventEndDate = query.eventEndDate.simpleDateFormat(), eventStatus = query.eventStatus?.toString(), trackedEntityType = query.trackedEntityType, - query = query.query, filter = toAPIFilterFormat(query.attributeFilter, upper = true), assignedUserMode = query.assignedUserMode?.toString(), lastUpdatedStartDate = query.lastUpdatedStartDate.simpleDateFormat(), @@ -222,8 +221,6 @@ internal class NewTrackedEntityEndpointCallFactory( orgUnitMode = query.orgUnitMode, program = query.program, uids = events.mapNotNull { it.trackedEntity() }.distinct(), - query = query.query, - attributeFilter = query.attributeFilter, order = query.order, trackedEntityType = query.trackedEntityType, includeDeleted = query.includeDeleted, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt index e43f059bab..11d3d01a62 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt @@ -99,7 +99,6 @@ internal interface TrackedEntityInstanceService { @Query(EVENT_END_DATE) eventEndDate: String?, @Query(EVENT_STATUS) eventStatus: String?, @Query(TRACKED_ENTITY_TYPE) trackedEntityType: String?, - @Query(QUERY) query: String?, @Query(FILTER) filter: List?, @Query(ASSIGNED_USER_MODE) assignedUserMode: String?, @Query(LAST_UPDATED_START_DATE) lastUpdatedStartDate: String?, @@ -116,7 +115,6 @@ internal interface TrackedEntityInstanceService { const val OU = "ou" const val OU_MODE = "ouMode" const val FIELDS = "fields" - const val QUERY = "query" const val PAGING = "paging" const val PAGE = "page" const val PAGE_SIZE = "pageSize" diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt index c6c6bc4a77..f01f4e1ab5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt @@ -149,7 +149,6 @@ internal class TrackedEntityInstanceQueryCallFactory( eventEndDate = query.eventEndDate.simpleDateFormat(), eventStatus = getEventStatus(query), trackedEntityType = query.trackedEntityType, - query = query.query, filter = toAPIFilterFormat(query.attributeFilter, upper = true), assignedUserMode = query.assignedUserMode?.toString(), lastUpdatedStartDate = query.lastUpdatedStartDate.simpleDateFormat(), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnline.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnline.kt index 0244c00c12..5bf2ece3d4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnline.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnline.kt @@ -43,7 +43,6 @@ internal data class TrackedEntityInstanceQueryOnline( val orgUnitMode: OrganisationUnitMode? = null, val program: String? = null, val programStage: String? = null, - val query: String? = null, val attributeFilter: List = emptyList(), val dataValueFilter: List = emptyList(), val programStartDate: Date? = null, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt index 0ef9c9aac2..efe36f87c5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt @@ -83,8 +83,6 @@ internal class TrackedEntityInstanceQueryOnlineHelper( private fun getBaseQuery( scope: TrackedEntityInstanceQueryRepositoryScope, ): TrackedEntityInstanceQueryOnline { - val query = scope.query()?.let { query -> query.operator().apiUpperOperator + ":" + query.value() } - // EnrollmentStatus does not accepts a list of status but a single value in web API. val enrollmentStatus = scope.enrollmentStatus()?.getOrNull(0) @@ -95,7 +93,6 @@ internal class TrackedEntityInstanceQueryOnlineHelper( orgUnits = scope.orgUnits(), orgUnitMode = scope.orgUnitMode(), program = scope.program(), - query = query, attributeFilter = scope.filter(), dataValueFilter = scope.dataValue(), lastUpdatedStartDate = scope.lastUpdatedDate()?.let { dateFilterPeriodHelper.getStartDate(it) }, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchOperators.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchOperators.kt index e0ff967c51..4b41e78973 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchOperators.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchOperators.kt @@ -111,19 +111,7 @@ abstract class TrackedEntitySearchOperators internal constru }.eq(null) } - /** - * Add an "attribute" filter to the query. If this method is called several times, conditions are appended with - * AND connector. - * - * - * For example, - *


.byAttribute("uid1").eq("value1")

.byAttribute("uid2").eq("value2")

- * means that the instance must have attribute "uid1" with value "value1" **AND** attribute "uid2" with - * value "value2". - * - * @param attributeId Attribute uid to use in the filter - * @return Repository connector - */ + @Deprecated(message = "Use byFilter()", replaceWith = ReplaceWith("byFilter(attributeId)")) fun byAttribute(attributeId: String): EqLikeItemFilterConnector { return byFilter(attributeId) } @@ -147,11 +135,10 @@ abstract class TrackedEntitySearchOperators internal constru } } - /** - * Search tracked entity instances with **any** attribute matching the query. - * - * @return Repository connector - */ + @Deprecated( + message = "This property is ignored for online queries and will be ignored in offline queries soon. " + + "Please use byFilter to achieve a similar functionality.", + ) fun byQuery(): EqLikeItemFilterConnector { return connectorFactory.eqLikeItemC("") { filterItem: RepositoryScopeFilterItem -> scope.toBuilder().query(filterItem).build() diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index 285e22cfb5..2eeb9e63a1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -66,7 +66,6 @@ internal interface TrackerExporterService { @Query(EVENT_END_DATE) eventEndDate: String? = null, @Query(EVENT_STATUS) eventStatus: String? = null, @Query(TRACKED_ENTITY_TYPE) trackedEntityType: String? = null, - @Query(QUERY) query: String? = null, @Query(FILTER) filter: List? = null, @Query(ASSIGNED_USER_MODE) assignedUserMode: String? = null, @Query(UPDATED_AFTER) lastUpdatedStartDate: String? = null, @@ -132,7 +131,6 @@ internal interface TrackerExporterService { const val OU = "orgUnit" const val OU_MODE = "ouMode" const val FIELDS = "fields" - const val QUERY = "query" const val PAGING = "paging" const val PAGE = "page" const val PAGE_SIZE = "pageSize" diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt index 49f628f6ef..8e67307730 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt @@ -100,7 +100,6 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { incidentStartDate = Date(), incidentEndDate = Date(), trackedEntityType = "teiTypeStr", - query = "queryStr", attributeFilter = attribute, includeDeleted = false, lastUpdatedStartDate = Date(), @@ -247,7 +246,6 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { eq(query.eventEndDate.simpleDateFormat()), eq(expectedStatus?.toString()), eq(query.trackedEntityType), - eq(query.query), any(), eq(query.assignedUserMode?.toString()), eq(query.lastUpdatedStartDate.simpleDateFormat()), @@ -325,7 +323,6 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { anyOrNull(), anyOrNull(), anyOrNull(), - anyOrNull(), ) }.doAnswer(answer) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt index 98f62eab7b..e9e92f4c8a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt @@ -53,19 +53,6 @@ class TrackedEntityInstanceQueryOnlineHelperShould { onlineHelper = TrackedEntityInstanceQueryOnlineHelper(periodHelper) } - @Test - fun parse_query_in_api_format() { - val scope = queryBuilder - .query( - RepositoryScopeFilterItem.builder().key("").operator(FilterItemOperator.LIKE).value("filter").build(), - ) - .build() - val onlineQueries = onlineHelper.fromScope(scope) - - assertThat(onlineQueries.size).isEqualTo(1) - assertThat(onlineQueries[0].query).isEqualTo("LIKE:filter") - } - @Test fun parse_filters_using_in_operator() { val list = listOf( From 9b21e9d4170b9343acc598b6559c43827727c52c Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 15 Jan 2024 09:08:47 +0100 Subject: [PATCH 024/222] [ANDROSDK-1696] Restrict script to :core:sonarqube --- scripts/sonarqube.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sonarqube.sh b/scripts/sonarqube.sh index 7192029a1c..bc461046ee 100644 --- a/scripts/sonarqube.sh +++ b/scripts/sonarqube.sh @@ -37,4 +37,4 @@ then git fetch --no-tags --force --progress -- $url +refs/heads/$GIT_BRANCH_DEST:refs/remotes/$remote/$GIT_BRANCH_DEST fi -./gradlew sonarqube --stacktrace --no-daemon \ No newline at end of file +./gradlew :core:sonarqube --stacktrace --no-daemon \ No newline at end of file From 206a49e1c99abfde062a1ff7747b0a8a173045e4 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 15 Jan 2024 09:29:50 +0100 Subject: [PATCH 025/222] [ANDROSDK-1696] Revert previous commit --- scripts/sonarqube.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sonarqube.sh b/scripts/sonarqube.sh index bc461046ee..7192029a1c 100644 --- a/scripts/sonarqube.sh +++ b/scripts/sonarqube.sh @@ -37,4 +37,4 @@ then git fetch --no-tags --force --progress -- $url +refs/heads/$GIT_BRANCH_DEST:refs/remotes/$remote/$GIT_BRANCH_DEST fi -./gradlew :core:sonarqube --stacktrace --no-daemon \ No newline at end of file +./gradlew sonarqube --stacktrace --no-daemon \ No newline at end of file From 599ba40429a999843643304d7528a3feac7ef4af Mon Sep 17 00:00:00 2001 From: andresmr Date: Mon, 4 Dec 2023 11:50:33 +0100 Subject: [PATCH 026/222] [ANDROSDK-1789] Add paging3 implementation --- .../collection/PagingMockIntegrationShould.kt | 88 +++++++++++++++++ ...llectionRepositoryMockIntegrationShould.kt | 17 ++++ .../ReadOnlyCollectionRepository.kt | 9 ++ .../ReadOnlyCollectionRepositoryImpl.kt | 17 ++++ ...WithTransformerCollectionRepositoryImpl.kt | 17 ++++ .../paging/internal/RepositoryPagingSource.kt | 93 ++++++++++++++++++ .../RepositoryPagingSourceWithTransformer.kt | 97 +++++++++++++++++++ .../search/EventQueryCollectionRepository.kt | 6 ++ ...EntityInstanceQueryCollectionRepository.kt | 26 +++++ .../TrackedEntityInstanceQueryPagingSource.kt | 85 ++++++++++++++++ ...TrackedEntitySearchCollectionRepository.kt | 26 +++++ .../search/TrackedEntitySearchPagingSource.kt | 87 +++++++++++++++++ 12 files changed, 568 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSource.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSourceWithTransformer.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/PagingMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/PagingMockIntegrationShould.kt index 46dc2839fe..5b73236362 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/PagingMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/repositories/collection/PagingMockIntegrationShould.kt @@ -29,18 +29,22 @@ package org.hisp.dhis.android.core.arch.repositories.collection import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.paging.PagedList +import androidx.paging.testing.asSnapshot import com.jraska.livedata.TestObserver +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.hisp.dhis.android.core.arch.db.querybuilders.internal.OrderByClauseBuilder import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.category.internal.CategoryOptionStore import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +@OptIn(ExperimentalCoroutinesApi::class) class PagingMockIntegrationShould : BaseMockIntegrationTestFullDispatcher() { @get:Rule @@ -152,4 +156,88 @@ class PagingMockIntegrationShould : BaseMockIntegrationTestFullDispatcher() { .assertValue { pagedList: PagedList -> pagedList[4]!!.displayName() == "Male" } .assertValue { pagedList: PagedList -> pagedList[5]!!.displayName() == "MCH Aides" } } + + @Test + fun get_pagingData_with_initial_objects_with_default_order_considering_prefetch_distance() = runTest { + val items = d2.categoryModule().categoryOptions().getPagingData(2) + + val itemsSnapshot: List = items.asSnapshot() + + assertEquals(itemsSnapshot.size, 6) + assertEquals(itemsSnapshot[0], allValues[0]) + assertEquals(itemsSnapshot[1], allValues[1]) + assertEquals(itemsSnapshot[2], allValues[2]) + assertEquals(itemsSnapshot[3], allValues[3]) + assertEquals(itemsSnapshot[4], allValues[4]) + assertEquals(itemsSnapshot[5], allValues[5]) + } + + @Test + fun get_pagingData_with_initial_objects_ordered_by_display_name_asc() = runTest { + val items = d2.categoryModule().categoryOptions() + .orderByDisplayName(RepositoryScope.OrderByDirection.ASC) + .getPagingData(2) + + val snapshot = items.asSnapshot() + + assertEquals(snapshot.size, 6) + assertEquals(snapshot[0].displayName(), "At PHU") + assertEquals(snapshot[1].displayName(), "Female") + assertEquals(snapshot[2].displayName(), "In Community") + assertEquals(snapshot[3].displayName(), "MCH Aides") + assertEquals(snapshot[4].displayName(), "Male") + assertEquals(snapshot[5].displayName(), "SECHN") + } + + @Test + fun get_pagingData_with_initial_objects_ordered_by_display_name_desc() = runTest { + val items = d2.categoryModule().categoryOptions() + .orderByDisplayName(RepositoryScope.OrderByDirection.DESC) + .getPagingData(2) + + val snapshot = items.asSnapshot() + + assertEquals(snapshot.size, 6) + assertEquals(snapshot[0].displayName(), "default display name") + assertEquals(snapshot[1].displayName(), "Trained TBA") + assertEquals(snapshot[2].displayName(), "SECHN") + assertEquals(snapshot[3].displayName(), "Male") + assertEquals(snapshot[4].displayName(), "MCH Aides") + assertEquals(snapshot[5].displayName(), "In Community") + } + + @Test + fun get_pagingData_with_initial_objects_ordered_by_description_desc() = runTest { + val items = d2.categoryModule().categoryOptions() + .orderByDescription(RepositoryScope.OrderByDirection.DESC) + .getPagingData(2) + + val snapshot = items.asSnapshot() + + assertEquals(snapshot.size, 6) + assertEquals(snapshot[0].displayName(), "default display name") + assertEquals(snapshot[1].displayName(), "Female") + assertEquals(snapshot[2].displayName(), "Male") + assertEquals(snapshot[3].displayName(), "In Community") + assertEquals(snapshot[4].displayName(), "At PHU") + assertEquals(snapshot[5].displayName(), "MCH Aides") + } + + @Test + fun get_pagingData_with_initial_objects_ordered_by_description_and_display_name_desc() = runTest { + val items = d2.categoryModule().categoryOptions() + .orderByDescription(RepositoryScope.OrderByDirection.DESC) + .orderByDisplayName(RepositoryScope.OrderByDirection.ASC) + .getPagingData(2) + + val snapshot = items.asSnapshot() + + assertEquals(snapshot.size, 6) + assertEquals(snapshot[0].displayName(), "default display name") + assertEquals(snapshot[1].displayName(), "At PHU") + assertEquals(snapshot[2].displayName(), "Female") + assertEquals(snapshot[3].displayName(), "In Community") + assertEquals(snapshot[4].displayName(), "Male") + assertEquals(snapshot[5].displayName(), "MCH Aides") + } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepositoryMockIntegrationShould.kt index 7a9d3545e2..7f6cc8423b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepositoryMockIntegrationShould.kt @@ -29,9 +29,15 @@ package org.hisp.dhis.android.core.trackedentity.search import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.paging.PagedList +import androidx.paging.PagingData +import androidx.paging.testing.asSnapshot import com.jraska.livedata.TestObserver +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.test.runTest import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -50,4 +56,15 @@ class TrackedEntityInstanceQueryCollectionRepositoryMockIntegrationShould : Base .assertHasValue() .assertValue { pagedList: PagedList -> pagedList.size == 2 } } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun get_PagingData_with_offline_initial_objects() = runTest { + val items: Flow> = d2.trackedEntityModule().trackedEntityInstanceQuery() + .offlineOnly().getPagingData(2) + + val snapshot = items.asSnapshot() + + assertEquals(snapshot.size, 2) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyCollectionRepository.kt index d242f752e6..6fc97e38a9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/ReadOnlyCollectionRepository.kt @@ -29,7 +29,9 @@ package org.hisp.dhis.android.core.arch.repositories.collection import androidx.lifecycle.LiveData import androidx.paging.PagedList +import androidx.paging.PagingData import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyObjectRepository interface ReadOnlyCollectionRepository : BaseRepository { @@ -57,6 +59,13 @@ interface ReadOnlyCollectionRepository : BaseRepository { @Deprecated(message = "Use {@link #getPagingData()} instead}", replaceWith = ReplaceWith("getPagingData()")) fun getPaged(pageSize: Int): LiveData> + /** + * Uses Paging3 library and return a Flow + * @param pageSize Length of the page + * @return a Flow of PagingData elements + */ + fun getPagingData(pageSize: Int): Flow> + /** * Get the count of elements in an asynchronous way, returning a `Single`. * @return A `Single` object with the element count diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt index 665b2743cd..00d6f1b7d9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt @@ -31,7 +31,12 @@ import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.querybuilders.internal.OrderByClauseBuilder import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder @@ -42,6 +47,7 @@ import org.hisp.dhis.android.core.arch.repositories.collection.ReadOnlyCollectio import org.hisp.dhis.android.core.arch.repositories.filters.internal.FilterConnectorFactory import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl import org.hisp.dhis.android.core.arch.repositories.paging.internal.RepositoryDataSource +import org.hisp.dhis.android.core.arch.repositories.paging.internal.RepositoryPagingSource import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder import org.hisp.dhis.android.core.common.CoreObject @@ -113,10 +119,21 @@ open class ReadOnlyCollectionRepositoryImpl> { + return Pager( + config = PagingConfig(pageSize = pageSize), + ) { + pagingSource + }.flow + } + @Deprecated("Use {@link #getPagingData()} instead}", replaceWith = ReplaceWith("getPagingData()")) val dataSource: DataSource get() = RepositoryDataSource(store, databaseAdapter, scope, childrenAppenders) + private val pagingSource: PagingSource + get() = RepositoryPagingSource(store, databaseAdapter, scope, childrenAppenders) + /** * Get the count of elements in an asynchronous way, returning a `Single`. * @return A `Single` object with the element count diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt index 2e3c34d678..851615644e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt @@ -31,7 +31,12 @@ import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.querybuilders.internal.OrderByClauseBuilder import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder @@ -44,6 +49,7 @@ import org.hisp.dhis.android.core.arch.repositories.filters.internal.FilterConne import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyObjectRepository import org.hisp.dhis.android.core.arch.repositories.`object`.internal.ReadOnlyWithTransformerObjectRepositoryImpl import org.hisp.dhis.android.core.arch.repositories.paging.internal.RepositoryDataSourceWithTransformer +import org.hisp.dhis.android.core.arch.repositories.paging.internal.RepositoryPagingSourceWithTransformer import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder import org.hisp.dhis.android.core.common.CoreObject @@ -126,9 +132,20 @@ internal open class ReadOnlyWithTransformerCollectionRepositoryImpl< return LivePagedListBuilder(factory, pageSize).build() } + override fun getPagingData(pageSize: Int): Flow> { + return Pager( + config = PagingConfig(pageSize = pageSize), + ) { + pagingSource + }.flow + } + val dataSource: DataSource get() = RepositoryDataSourceWithTransformer(store, databaseAdapter, scope, childrenAppenders, transformer) + private val pagingSource: PagingSource + get() = RepositoryPagingSourceWithTransformer(store, databaseAdapter, scope, childrenAppenders, transformer) + /** * Get the count of elements in an asynchronous way, returning a `Single`. * @return A `Single` object with the element count diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSource.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSource.kt new file mode 100644 index 0000000000..bd908852fd --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSource.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.repositories.paging.internal + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.OrderByClauseBuilder +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder +import org.hisp.dhis.android.core.arch.db.stores.internal.ReadableStore +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderExecutor.appendInObjectCollection +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder +import org.hisp.dhis.android.core.common.CoreObject +import java.io.IOException + +class RepositoryPagingSource internal constructor( + private val store: ReadableStore, + private val databaseAdapter: DatabaseAdapter, + private val scope: RepositoryScope, + private val childrenAppenders: ChildrenAppenderGetter, +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): M? { + return state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } + } + + override suspend fun load(params: LoadParams): LoadResult { + try { + val whereClauseBuilder = WhereClauseBuilder() + + params.key?.let { key -> + val reverse = when (params) { + is LoadParams.Prepend -> true + else -> false + } + + OrderByClauseBuilder.addSortingClauses( + whereClauseBuilder, + scope.orderBy(), + key.toContentValues(), + reverse, + scope.pagingKey(), + ) + } + + val whereClause = WhereClauseFromScopeBuilder(whereClauseBuilder).getWhereClause( + scope, + ) + val withoutChildren = store.selectWhere( + whereClause, + OrderByClauseBuilder.orderByFromItems(scope.orderBy(), scope.pagingKey()), + params.loadSize, + ) + + val items = appendInObjectCollection(withoutChildren, databaseAdapter, childrenAppenders, scope.children()) + return LoadResult.Page( + data = items, + prevKey = items.firstOrNull(), + nextKey = items.getOrNull(params.loadSize - 1), + ) + } catch (e: IOException) { + return LoadResult.Error(e) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSourceWithTransformer.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSourceWithTransformer.kt new file mode 100644 index 0000000000..f91b42b159 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/internal/RepositoryPagingSourceWithTransformer.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.repositories.paging.internal + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.OrderByClauseBuilder +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder +import org.hisp.dhis.android.core.arch.db.stores.internal.ReadableStore +import org.hisp.dhis.android.core.arch.handlers.internal.TwoWayTransformer +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderExecutor +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder +import org.hisp.dhis.android.core.common.CoreObject +import java.io.IOException + +internal class RepositoryPagingSourceWithTransformer internal constructor( + private val store: ReadableStore, + private val databaseAdapter: DatabaseAdapter, + private val scope: RepositoryScope, + private val childrenAppenders: ChildrenAppenderGetter, + private val transformer: TwoWayTransformer, +) : PagingSource() { + override fun getRefreshKey(state: PagingState): M? { + return state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } + } + + override suspend fun load(params: LoadParams): LoadResult { + try { + val whereClauseBuilder = WhereClauseBuilder() + + params.key?.let { key -> + val reverse = when (params) { + is LoadParams.Prepend -> true + else -> false + } + + OrderByClauseBuilder.addSortingClauses( + whereClauseBuilder, + scope.orderBy(), + key.toContentValues(), + reverse, + scope.pagingKey(), + ) + } + + val whereClause = WhereClauseFromScopeBuilder(whereClauseBuilder).getWhereClause(scope) + val withoutChildren = store.selectWhere( + whereClause, + OrderByClauseBuilder.orderByFromItems(scope.orderBy(), scope.pagingKey()), + params.loadSize, + ) + val items = ChildrenAppenderExecutor.appendInObjectCollection( + withoutChildren, + databaseAdapter, + childrenAppenders, + scope.children(), + ) + + return LoadResult.Page( + data = items.map { transformer.transform(it) }, + prevKey = items.firstOrNull(), + nextKey = items.getOrNull(params.loadSize - 1), + ) + } catch (e: IOException) { + return LoadResult.Error(e) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt index 74ab69e752..63388a078a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt @@ -30,7 +30,9 @@ package org.hisp.dhis.android.core.event.search import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.PagedList +import androidx.paging.PagingData import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.repositories.collection.ReadOnlyWithUidCollectionRepository import org.hisp.dhis.android.core.arch.repositories.filters.internal.EqFilterConnector import org.hisp.dhis.android.core.arch.repositories.filters.internal.EventDataFilterConnector @@ -271,6 +273,10 @@ class EventQueryCollectionRepository internal constructor( return eventCollectionRepository.getPaged(pageSize) } + override fun getPagingData(pageSize: Int): Flow> { + return eventCollectionRepository.getPagingData(pageSize) + } + val dataSource: DataSource get() = eventCollectionRepository.dataSource diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt index 6dcaccfc44..5a5fde9eec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt @@ -31,7 +31,12 @@ import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.handlers.internal.Transformer import org.hisp.dhis.android.core.arch.helpers.Result @@ -120,6 +125,7 @@ class TrackedEntityInstanceQueryCollectionRepository internal constructor( } } + @Deprecated("Use {@link #getPagingData()} instead}", replaceWith = ReplaceWith("getPagingData()")) override fun getPaged(pageSize: Int): LiveData> { val factory: DataSource.Factory = object : DataSource.Factory() { @@ -130,9 +136,29 @@ class TrackedEntityInstanceQueryCollectionRepository internal constructor( return LivePagedListBuilder(factory, pageSize).build() } + override fun getPagingData(pageSize: Int): Flow> { + return Pager( + config = PagingConfig(pageSize = pageSize), + ) { + pagingSource + }.flow + } + val dataSource: DataSource get() = TrackedEntityInstanceQueryDataSource(getDataFetcher()) + val pagingSource: PagingSource + get() = TrackedEntityInstanceQueryPagingSource( + store, + databaseAdapter, + trackerParentCallFactory, + scope, + childrenAppenders, + onlineCache, + onlineHelper, + localQueryHelper, + ) + @Deprecated("use getPagingdata") val resultDataSource: DataSource> get() = TrackedEntityInstanceQueryDataSourceResult(getDataFetcher()) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt new file mode 100644 index 0000000000..df6cb3bb4e --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.trackedentity.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityInstanceStore +import org.hisp.dhis.android.core.trackedentity.internal.TrackerParentCallFactory + +internal class TrackedEntityInstanceQueryPagingSource( + store: TrackedEntityInstanceStore, + databaseAdapter: DatabaseAdapter, + trackerParentCallFactory: TrackerParentCallFactory, + scope: TrackedEntityInstanceQueryRepositoryScope, + childrenAppenders: ChildrenAppenderGetter, + onlineCache: TrackedEntityInstanceOnlineCache, + onlineHelper: TrackedEntityInstanceQueryOnlineHelper, + localQueryHelper: TrackedEntityInstanceLocalQueryHelper, +) : PagingSource() { + + private val dataFetcher = TrackedEntityInstanceQueryDataFetcher( + store, + databaseAdapter, + trackerParentCallFactory, + scope, + childrenAppenders, + onlineCache, + onlineHelper, + localQueryHelper, + ) + + init { + dataFetcher.refresh() + } + + override fun getRefreshKey( + state: PagingState, + ): TrackedEntityInstance? { + return state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } + } + + override suspend fun load( + params: LoadParams, + ): LoadResult { + val pages = dataFetcher.loadPages(params.loadSize) + + return pages.firstOrNull { it is Result.Failure }?.let { + LoadResult.Error((it as Result.Failure).failure) + } ?: LoadResult.Page( + data = pages.map { it.getOrThrow() }, + prevKey = pages.firstOrNull()?.getOrThrow(), + nextKey = pages.getOrNull(params.loadSize - 1)?.getOrThrow(), + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt index 11eb47415c..c67d2854cc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt @@ -32,7 +32,12 @@ import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.handlers.internal.Transformer import org.hisp.dhis.android.core.arch.helpers.Result @@ -98,9 +103,30 @@ class TrackedEntitySearchCollectionRepository internal constructor( return LivePagedListBuilder(factory, pageSize).build() } + override fun getPagingData(pageSize: Int): Flow> { + return Pager( + config = PagingConfig(pageSize = pageSize), + ) { + pagingSource + }.flow + } + val dataSource: DataSource get() = TrackedEntitySearchDataSource(getDataFetcher()) + val pagingSource: PagingSource + get() = TrackedEntitySearchPagingSource( + store, + databaseAdapter, + trackerParentCallFactory, + scope, + childrenAppenders, + onlineCache, + onlineHelper, + localQueryHelper, + searchDataFetcherHelper, + ) + val resultDataSource: DataSource> get() = TrackedEntitySearchDataSourceResult(getDataFetcher()) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt new file mode 100644 index 0000000000..69c35de1ae --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.trackedentity.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityInstanceStore +import org.hisp.dhis.android.core.trackedentity.internal.TrackerParentCallFactory + +internal class TrackedEntitySearchPagingSource( + store: TrackedEntityInstanceStore, + databaseAdapter: DatabaseAdapter, + trackerParentCallFactory: TrackerParentCallFactory, + scope: TrackedEntityInstanceQueryRepositoryScope, + childrenAppenders: ChildrenAppenderGetter, + onlineCache: TrackedEntityInstanceOnlineCache, + onlineHelper: TrackedEntityInstanceQueryOnlineHelper, + localQueryHelper: TrackedEntityInstanceLocalQueryHelper, + helper: TrackedEntitySearchDataFetcherHelper, +) : PagingSource() { + + private val dataFetcher = TrackedEntitySearchDataFetcher( + store, + databaseAdapter, + trackerParentCallFactory, + scope, + childrenAppenders, + onlineCache, + onlineHelper, + localQueryHelper, + helper, + ) + + init { + dataFetcher.refresh() + } + + override fun getRefreshKey( + state: PagingState, + ): TrackedEntitySearchItem? { + return state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } + } + + override suspend fun load( + params: LoadParams, + ): LoadResult { + val pages = dataFetcher.loadPages(params.loadSize) + + return pages.firstOrNull { it is Result.Failure }?.let { + LoadResult.Error((it as Result.Failure).failure) + } ?: LoadResult.Page( + data = pages.map { it.getOrThrow() }, + prevKey = pages.firstOrNull()?.getOrThrow(), + nextKey = pages.getOrNull(params.loadSize - 1)?.getOrThrow(), + ) + } +} From e41030b4df682191040929c3cf4a761e72e0d300 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 12 Dec 2023 10:13:03 +0100 Subject: [PATCH 027/222] [PR-ANDROSDK-1789] Publish 1789 snapshot version --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index fe0e0853db..9df3819b14 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -74,6 +74,7 @@ pipeline { expression { env.GIT_BRANCH == "master" } expression { env.GIT_BRANCH == "develop" } expression { env.GIT_BRANCH ==~ /[0-9]+\.[0-9]+\.[0-9]+-rc/ } + expression { env.GIT_BRANCH == "PR-ANDROSDK-1789" } } } } From 5859834723324056b7836c67bf8d11879d47369b Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 12 Dec 2023 10:27:35 +0100 Subject: [PATCH 028/222] [PR-ANDROSDK-1789] Dummy change to trigger deploy --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 9df3819b14..5a48328cfb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -74,6 +74,7 @@ pipeline { expression { env.GIT_BRANCH == "master" } expression { env.GIT_BRANCH == "develop" } expression { env.GIT_BRANCH ==~ /[0-9]+\.[0-9]+\.[0-9]+-rc/ } + expression { env.GIT_BRANCH == "PR-ANDROSDK-1789" } } } From 3670c203477e7955f83b3e21baa597a665837258 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 14 Dec 2023 14:39:21 +0100 Subject: [PATCH 029/222] [PR-ANDROSDK-1789] Expose Pager --- .../internal/ReadOnlyCollectionRepositoryImpl.kt | 7 ++++++- .../ReadOnlyWithTransformerCollectionRepositoryImpl.kt | 9 +++++++++ .../core/event/search/EventQueryCollectionRepository.kt | 5 +++++ .../TrackedEntityInstanceQueryCollectionRepository.kt | 6 +++++- .../search/TrackedEntitySearchCollectionRepository.kt | 6 +++++- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt index 00d6f1b7d9..2992f48403 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyCollectionRepositoryImpl.kt @@ -52,6 +52,7 @@ import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder import org.hisp.dhis.android.core.common.CoreObject +@Suppress("TooManyFunctions") open class ReadOnlyCollectionRepositoryImpl> internal constructor( private val store: ReadableStore, internal val databaseAdapter: DatabaseAdapter, @@ -120,11 +121,15 @@ open class ReadOnlyCollectionRepositoryImpl> { + return getPager(pageSize).flow + } + + fun getPager(pageSize: Int): Pager { return Pager( config = PagingConfig(pageSize = pageSize), ) { pagingSource - }.flow + } } @Deprecated("Use {@link #getPagingData()} instead}", replaceWith = ReplaceWith("getPagingData()")) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt index 851615644e..29c3469661 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/collection/internal/ReadOnlyWithTransformerCollectionRepositoryImpl.kt @@ -54,6 +54,7 @@ import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.arch.repositories.scope.internal.WhereClauseFromScopeBuilder import org.hisp.dhis.android.core.common.CoreObject +@Suppress("TooManyFunctions") internal open class ReadOnlyWithTransformerCollectionRepositoryImpl< M : CoreObject, T : Any, @@ -140,6 +141,14 @@ internal open class ReadOnlyWithTransformerCollectionRepositoryImpl< }.flow } + fun getPager(pageSize: Int): Pager { + return Pager( + config = PagingConfig(pageSize = pageSize), + ) { + pagingSource + } + } + val dataSource: DataSource get() = RepositoryDataSourceWithTransformer(store, databaseAdapter, scope, childrenAppenders, transformer) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt index 63388a078a..923be7cad8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/search/EventQueryCollectionRepository.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.event.search import androidx.lifecycle.LiveData import androidx.paging.DataSource import androidx.paging.PagedList +import androidx.paging.Pager import androidx.paging.PagingData import io.reactivex.Single import kotlinx.coroutines.flow.Flow @@ -277,6 +278,10 @@ class EventQueryCollectionRepository internal constructor( return eventCollectionRepository.getPagingData(pageSize) } + fun getPager(pageSize: Int): Pager { + return eventCollectionRepository.getPager(pageSize) + } + val dataSource: DataSource get() = eventCollectionRepository.dataSource diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt index 5a5fde9eec..7a6b6dabb6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCollectionRepository.kt @@ -137,11 +137,15 @@ class TrackedEntityInstanceQueryCollectionRepository internal constructor( } override fun getPagingData(pageSize: Int): Flow> { + return getPager(pageSize).flow + } + + fun getPager(pageSize: Int): Pager { return Pager( config = PagingConfig(pageSize = pageSize), ) { pagingSource - }.flow + } } val dataSource: DataSource diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt index c67d2854cc..e4c30a3e5c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchCollectionRepository.kt @@ -104,11 +104,15 @@ class TrackedEntitySearchCollectionRepository internal constructor( } override fun getPagingData(pageSize: Int): Flow> { + return getPager(pageSize).flow + } + + fun getPager(pageSize: Int): Pager { return Pager( config = PagingConfig(pageSize = pageSize), ) { pagingSource - }.flow + } } val dataSource: DataSource From d972fb35c5a2f60d3878257ca25e0dfe837a35e4 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 19 Jan 2024 08:49:06 +0100 Subject: [PATCH 030/222] [PR-ANDROSDK-1789] Update to paging 3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70a7d9a925..1a2e931d0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ libraryDesugaring = "2.0.4" # android annotation = "1.6.0" -paging = "2.1.2" +paging = "3.2.1" # java jackson = "2.13.4" From 61cbdab4aa4436576270dea56704ae6d09f9c32d Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 19 Jan 2024 10:12:43 +0100 Subject: [PATCH 031/222] [PR-ANDROSDK-1789] Add paging testing dependency --- core/build.gradle.kts | 3 ++- gradle/libs.versions.toml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8516146216..d85e4cc5f4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -123,7 +123,7 @@ dependencies { // AndroidX api(libs.androidx.annotation) - api(libs.androidx.paging) + api(libs.androidx.paging.runtime) // Auto Value api(libs.google.auto.value.annotation) @@ -188,6 +188,7 @@ dependencies { exclude(group = "junit") // Android has JUnit built in. } androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.androidx.paging.testing) debugImplementation(libs.facebook.soloader) debugImplementation(libs.facebook.flipper.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a2e931d0e..ed9329f188 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,8 @@ rx-java = { group = "io.reactivex.rxjava2", name = "rxjava", version.ref = "rxJa rx-android = { group = "io.reactivex.rxjava2", name = "rxandroid", version.ref = "rxAndroid" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } -androidx-paging = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +androidx-paging-testing = { group = "androidx.paging", name = "paging-testing", version.ref = "paging" } google-auto-value-annotation = { group = "com.google.auto.value", name = "auto-value-annotations", version.ref = "autoValue" } google-auto-value = { group = "com.google.auto.value", name = "auto-value", version.ref = "autoValue" } From 55d9a5e5626ddc0865a2b74e21e75a9c1d8f33b9 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 19 Jan 2024 10:40:15 +0100 Subject: [PATCH 032/222] [PR-ANDROSDK-1789] Change version --- core/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/gradle.properties b/core/gradle.properties index 212f0f2af5..eba77ea4df 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -29,7 +29,7 @@ # Properties which are consumed by plugins/gradle-mvn-push.gradle plugin. # They are used for publishing artifact to snapshot repository. -VERSION_NAME=1.10.0-SNAPSHOT +VERSION_NAME=1.9.1-1789-SNAPSHOT VERSION_CODE=292 GROUP=org.hisp.dhis From a0d15dc1e6b51a414c4b087ecf837bc8c5aeaf60 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 19 Jan 2024 08:40:10 +0100 Subject: [PATCH 033/222] [ANDROSDK-1218] Adapt database exporter --- ...FromDatabaseAssetsMockIntegrationShould.kt | 27 ++++- .../access/internal/DatabaseExportMetadata.kt | 36 ++++++ .../internal/DatabaseImportExportImpl.kt | 35 +++++- .../core/maintenance/MaintenanceModule.java | 4 +- .../internal/MaintenanceModuleImpl.kt | 6 + .../hisp/dhis/android/core/util/FileUtils.kt | 46 ++++++++ .../android/core/analytics/CipherTest.java | 103 ++++++++++++++++++ 7 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt index 0ceef2c025..5e73d10fb5 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt @@ -27,10 +27,23 @@ */ package org.hisp.dhis.android.core.arch.db.access.internal -// @RunWith(D2JunitRunner::class) +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.D2Factory +import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.mockwebserver.Dhis2MockServer +import org.hisp.dhis.android.core.systeminfo.SystemInfoTableInfo +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.After +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould { - /*companion object LocalAnalyticsAggregatedLargeDataMockIntegrationShould { + companion object LocalAnalyticsAggregatedLargeDataMockIntegrationShould { val context = InstrumentationRegistry.getInstrumentation().context val server = Dhis2MockServer(60809) @@ -119,11 +132,13 @@ class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould { assertThat(d2.programModule().programs().blockingCount()).isEqualTo(2) val systemInfoWithExpectedContextPath = d2.systemInfoModule().systemInfo().blockingGet() - .toBuilder().contextPath(serverUrl).build() + ?.toBuilder()?.contextPath(serverUrl)?.build() d2.databaseAdapter().delete(SystemInfoTableInfo.TABLE_INFO.name()) - d2.databaseAdapter().insert(SystemInfoTableInfo.TABLE_INFO.name(), null, - systemInfoWithExpectedContextPath.toContentValues()) + d2.databaseAdapter().insert( + SystemInfoTableInfo.TABLE_INFO.name(), null, + systemInfoWithExpectedContextPath?.toContentValues() + ) val exportedFile = d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() @@ -140,5 +155,5 @@ class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould { d2.userModule().blockingLogIn("android", "Android123", serverUrl) assertThat(d2.programModule().programs().blockingCount()).isEqualTo(2) - }*/ + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt new file mode 100644 index 0000000000..157fc223a3 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.db.access.internal + +internal data class DatabaseExportMetadata( + val version: String, + val date: String, + val serverUrl: String, + val username: String, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index 668551d8e6..8e3e07acee 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -28,8 +28,13 @@ package org.hisp.dhis.android.core.arch.db.access.internal import android.content.Context +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toPath +import okio.buffer import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport +import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory.objectMapper import org.hisp.dhis.android.core.arch.storage.internal.CredentialsSecureStore import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationInsecureStore @@ -43,8 +48,11 @@ import org.hisp.dhis.android.core.maintenance.D2ErrorComponent import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoStoreImpl import org.hisp.dhis.android.core.user.UserModule import org.hisp.dhis.android.core.user.internal.UserStoreImpl +import org.hisp.dhis.android.core.util.FileUtils +import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton import java.io.File +import java.util.Date @Singleton internal class DatabaseImportExportImpl( @@ -61,6 +69,7 @@ internal class DatabaseImportExportImpl( companion object { const val TmpDatabase = "tmp-database.db" const val ExportDatabase = "export-database.db" + const val ExportMetadata = "export-metadata.json" } private val d2ErrorBuilder = D2Error.builder() @@ -131,9 +140,9 @@ internal class DatabaseImportExportImpl( val credentials = credentialsStore.get() val databasesConfiguration = databaseConfigurationSecureStore.get() val userConfiguration = DatabaseConfigurationHelper.getLoggedAccount( - databasesConfiguration, - credentials.serverUrl, - credentials.username, + configuration = databasesConfiguration, + username = credentials.username, + serverUrl = credentials.serverUrl, ) if (userConfiguration.encrypted()) { @@ -146,6 +155,24 @@ internal class DatabaseImportExportImpl( databaseAdapter.close() val databaseName = userConfiguration.databaseName() - return databaseRenamer.copyDatabase(databaseName, ExportDatabase) + val copiedDatabase = databaseRenamer.copyDatabase(databaseName, ExportDatabase) + + val metadata = DatabaseExportMetadata( + version = "V1", + date = Date().simpleDateFormat()!!, + serverUrl = credentials.serverUrl, + username = credentials.username, + ) + + val exportMetadataPath = copiedDatabase.parentFile?.let { "${it.path}/${ExportMetadata}".toPath() } + FileSystem.SYSTEM.sink(exportMetadataPath!!).use { sinkFile -> + sinkFile.buffer().use { bufferedSinkFile -> + bufferedSinkFile.writeUtf8(objectMapper().writeValueAsString(metadata)) + } + } + + FileUtils.zipFiles(exportMetadataPath, exportMetadataPath.parent!!.resolve("zipped.zip")) + + return copiedDatabase } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java index 7fc4e5bdea..f78d89ff32 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java @@ -27,6 +27,8 @@ */ package org.hisp.dhis.android.core.maintenance; +import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport; + public interface MaintenanceModule { ForeignKeyViolationCollectionRepository foreignKeyViolations(); D2ErrorCollectionRepository d2Errors(); @@ -34,5 +36,5 @@ PerformanceHintsService getPerformanceHintsService(int organisationUnitThreshold int programRulesPerProgramThreshold); - // TODO restore when finished DatabaseImportExport databaseImportExport(); + DatabaseImportExport databaseImportExport(); } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/internal/MaintenanceModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/maintenance/internal/MaintenanceModuleImpl.kt index a388f4c777..8d0f516acb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/internal/MaintenanceModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/internal/MaintenanceModuleImpl.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.maintenance.internal import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport import org.hisp.dhis.android.core.maintenance.D2ErrorCollectionRepository import org.hisp.dhis.android.core.maintenance.ForeignKeyViolationCollectionRepository import org.hisp.dhis.android.core.maintenance.MaintenanceModule @@ -39,6 +40,7 @@ internal class MaintenanceModuleImpl( private val databaseAdapter: DatabaseAdapter, private val foreignKeyViolations: ForeignKeyViolationCollectionRepository, private val d2Errors: D2ErrorCollectionRepository, + private val databaseImportExport: DatabaseImportExport, ) : MaintenanceModule { override fun getPerformanceHintsService( organisationUnitThreshold: Int, @@ -58,4 +60,8 @@ internal class MaintenanceModuleImpl( override fun d2Errors(): D2ErrorCollectionRepository { return d2Errors } + + override fun databaseImportExport(): DatabaseImportExport { + return databaseImportExport + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt new file mode 100644 index 0000000000..3f6d724595 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +import okio.FileSystem +import okio.GzipSink +import okio.Okio +import okio.Path +import okio.buffer +import okio.use + +internal object FileUtils { + fun zipFiles(file: Path, target: Path) { + FileSystem.SYSTEM.sink(target).use { sink -> + GzipSink(sink).buffer().use { gzipBuffer -> + gzipBuffer.writeAll(FileSystem.SYSTEM.source(file)) + } + } + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java b/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java new file mode 100644 index 0000000000..45e9ec6753 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics; + +import org.junit.Test; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class CipherTest { + + private static byte[] salt = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + + private static String encryptMessageGH(String message, String password) throws Exception { + byte[] iv = salt; + SecretKey aesKey = getAESKeyFromPassword(password, iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv)); + byte[] ciphertext = cipher.doFinal(message.getBytes()); + byte[] encrypted = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, encrypted, 0, iv.length); + System.arraycopy(ciphertext, 0, encrypted, iv.length, ciphertext.length); + return Base64.getEncoder().encodeToString(encrypted); + } + + private static String decryptMessageGH(String encryptedMessage, String password) throws Exception { + byte[] encrypted = Base64.getDecoder().decode(encryptedMessage); + byte[] iv = salt; + SecretKey aesKey = getAESKeyFromPassword(password, iv); + System.arraycopy(encrypted, 0, iv, 0, iv.length); + byte[] ciphertext = new byte[encrypted.length - iv.length]; + System.arraycopy(encrypted, iv.length, ciphertext, 0, ciphertext.length); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv)); + return new String(cipher.doFinal(ciphertext), "UTF-8"); + } + + public static SecretKey getAESKeyFromPassword(String password, byte[] salt) + throws NoSuchAlgorithmException, InvalidKeySpecException { + + long start = System.currentTimeMillis(); + + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + // iterationCount = 1000 + // keyLength = 256 + int ITERATION_COUNT = 100000; + int keyLength = 256; + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, keyLength); + SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + + long end = System.currentTimeMillis(); + + System.out.println("Key generated in " + (end - start)); + + return key; + } + + @Test + public void cipher_test() throws Exception { + String orig = "Test message"; + String enc = encryptMessageGH(orig, "abcdef1234"); + System.out.println("Encrypted: " + enc); + String dec = decryptMessageGH(enc, "abcdef1234"); + System.out.println("Decrypted: " + dec); + } +} From 3c108191d0f9d1e9f3c7b18f34e37fd564874d98 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 23 Jan 2024 15:29:20 +0100 Subject: [PATCH 034/222] [ANDROSDK-1218] Redefine import/export flow --- .../assets/databases/corrupted-database.zip | Bin 0 -> 239 bytes .../assets/databases/export-database.zip | Bin 0 -> 1016537 bytes .../core/MockIntegrationTestObjects.kt | 3 +- ...FromDatabaseAssetsMockIntegrationShould.kt | 167 ++++++++++-------- .../access/internal/TestDatabaseImporter.kt | 50 +++--- .../mock/BaseMockIntegrationTest.kt | 4 +- .../MockIntegrationTestDatabaseContent.java | 3 +- .../mock/MockIntegrationTestObjectsFactory.kt | 4 +- .../{internal => }/DatabaseExportMetadata.kt | 7 +- .../arch/db/access/DatabaseImportExport.kt | 2 +- .../internal/DatabaseImportExportImpl.kt | 158 ++++++++++------- .../internal/DatabaseAccount.java | 6 + .../internal/DatabaseAccountImportDB.java | 65 +++++++ .../internal/DatabaseAccountImportStatus.kt | 34 ++++ .../internal/DatabaseConfigurationHelper.kt | 24 ++- .../internal/MultiUserDatabaseManager.kt | 90 +++++++--- .../android/core/maintenance/D2ErrorCode.java | 2 + ...enanceModule.java => MaintenanceModule.kt} | 21 +-- .../android/core/user/internal/LogInCall.kt | 31 +++- .../user/internal/LogInDatabaseManager.kt | 12 ++ .../hisp/dhis/android/core/util/CipherUtil.kt | 90 ++++++++++ .../hisp/dhis/android/core/util/FileUtils.kt | 75 ++++++-- .../android/core/analytics/CipherTest.java | 103 ----------- .../DatabasesConfigurationHelperShould.kt | 6 +- .../MultiUserDatabaseManagerUnitShould.kt | 8 +- .../android/core/util/CipherUtilShould.kt | 41 +++++ 26 files changed, 666 insertions(+), 340 deletions(-) create mode 100644 core/src/androidTest/assets/databases/corrupted-database.zip create mode 100644 core/src/androidTest/assets/databases/export-database.zip rename core/src/main/java/org/hisp/dhis/android/core/arch/db/access/{internal => }/DatabaseExportMetadata.kt (92%) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportDB.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportStatus.kt rename core/src/main/java/org/hisp/dhis/android/core/maintenance/{MaintenanceModule.java => MaintenanceModule.kt} (79%) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/util/CipherUtil.kt delete mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java create mode 100644 core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt diff --git a/core/src/androidTest/assets/databases/corrupted-database.zip b/core/src/androidTest/assets/databases/corrupted-database.zip new file mode 100644 index 0000000000000000000000000000000000000000..985beee22fc474791d1344cd91f621e2c750c515 GIT binary patch literal 239 zcmWIWW@Zs#VBp|js7N-CaL(jzPi0_W$OmGeC_`#RL4Hw*ZfUexB&* zi(JhHJgpDQPj0;wH8-cS-_{{RnJZR1Q?2LWZt48<|L5Av=;W?uEvPsns}jW4^5F25 z-wXMbOD;t4>{X492rVzsyY5lB*0=DR|9_(|rlBv2=dV;*8NF$u_munV<}iq#>OIdL i;LXS+0&)TlhXLJ&1h6?Xz?+o~q=XR&oq+UouzCQts!2cq literal 0 HcmV?d00001 diff --git a/core/src/androidTest/assets/databases/export-database.zip b/core/src/androidTest/assets/databases/export-database.zip new file mode 100644 index 0000000000000000000000000000000000000000..fd3785920c9f28c538b179fb1097a2a71fc23168 GIT binary patch literal 1016537 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%0AN@RA11lXvb?nth_l7R0LjL7d`3nuD}>`2^_B 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�T5ymJd`Wtq369G9?|qkAoXPvl|LMHplR9 zjfJx$uN9&5WS~dNErPDD^$}uk0q_lJpHc?tIgjeH9sQcq+ET6we?hSM}NWcIEYV!+! z+*Y>V)8P^q4ilS8I^9zlu&4ly^Aq3grwPUkIu}YP;T%dc7qi#d^fx>)xah3rK5Y&_tMYDWQve(l87&wN!N@X=wl;oP^)ideU?ktGE`v!<9 zPHnEr9DzNq1_xMY7A(jj8dNNJ_2Yn<<^TTK-5)Ha>de?U95e7K^e(F_gdiHjH-2+E zgP#clh3A7BmJ<{@hLp9mGf%hLx}r*(519z;Ov8&R4IvZ6o&~UYPqZBZh}hUiwtzeFZu9N*;w>~*%kWVS^Talz^%V%pTfp#8XfIhuDl3@8 z*sBUZF`b1;YA)cySYeIFRxyf!cNUYnejLBmAM+x2hiKthtwY+)1}17IIk~5RrO}$r zh3g~QTd}I=J1jcp%n6F6Zu`-qvWRFQE$Dxm_50UJ@3eERxvkPp6arRByU3B$6ESVP z7cJc7C1%g?t8VuI=#cL}FKB;TFnbdX7a)Q|^~3Ebb@a+A(U@LSG}swlrO~tlE2*gy zJO&uc+4ugem7ZNL@C=jHiB#Oa216=iN5M1PwYv3mvdTvEW^$1(^3h_J?#!gROfS?3Rvo zOxdY!)bZ`~bbY$Xdjyw=cpL3oA7S8gcI7^PxLe+cl)Xg9VeBL!mE$@=+-wJN>wJsj zb7~=UyCSS=>(!WI^&@pis%yTEnNovCA#7={S`Xx;TMNq)3uZJnr*j8yhGioy?zLK%Mv zYJIOBnEv1t7wQZWa&~XEajcVW3x}@INzMyb#o#vkx#AeJJDdkRU99cR0>G#nE>_Bh zurd-;rLy{4V{r{4gL8hC8s%KGy2U0W!b;}2eSE~3mwi+Wjunf4F_HL}orK_#83853 z`t!&HM|9=NwlW(Wj@8GwCDuC6RlA0KyS--y7U*+`g3l` zm3gU`3{JkJFwa&BdB_mFvkJL`$8=!;yFK7$aI}QuQ-j$Vh2#KFC)!i%Q4@`(K+XSF zW0LIS!?rXL^4faX#sXJe&esNhPahPI3R4aDpg0FaF)4WibgmE{p@D`;4 z)#rEv67O86eRo+F^XbUCMbOFtm0`q+KKl}-ogdE5m zV7xp9L|um|iYN5-`LIX+_+xWje>cna6AG@M*guSz3MvJ9u731}QtrARsr`v!Hl!te}lTLe<>OjYpNUC*K))zB%1A zbI756ZFO~p2#3c~7HU+oUC?AsNjSU%W*c}OZOkVGOG4d7qy^%rcakYd<+b=~)a@G! z+Z{5RA)@CEa2c1s{owFF9Bi^Edj`YneoNJ7Q)v*ZUzH5*aH;x66!L2J^(XiU36Cf3O< zkM?;1#{i@&0e~@h4n9W^BC32Qfj>ro_{k1pB7l1FI?|w1YjnX~N9Z%y2^=FhWv}HE zL*weIo$pwr7l|`8FX+;EeOc=jxZVAl%@%r4F}rx^4WZ1j5z{|41*p0o;GJU%`u;v+ zHyykcU+#OivSI&sFEWAdpi9Kt1NlI4wqqm^ZtUp$Rxo917y%k!nV8n(F2Zu+oB?~1 zGzN4d(uvW|5xxj6MV1gH$^s=6Su&SHI}YW(BxE0f@r`dQV#kXq?L5~lWiO|tDuRyD z+al1)b(fRpX`BC^XEW(Ol}kW47;!@Lgk`sRW3y%ztB}beR9u){5v=o&U10_gXpl<^ zvC2wEHSDS#!Ao{^JZ-=u+y$pO;kke;`LdYw_B;%vX3DbcDirX0A*b8wXCiqFDhZ7? z7QCAnJjga-!rk3b2|oIH?bSVUW@;n2DQ!j(EX1W2l(b^a!`e~_G?CqzO#!AO=S>< zWkz)+if5-L@{JU^bzxtUf|EP$j_WvHwUVyfyr!4Hg|5o>q(VybHV|D}pG^8S`6 zQqU6tLK=+eq{Dx0WU4_LKnXKD@Z6F7gl+R;%};pbm0)tQpBK7>Vd{;VwLTOZ(o=+w zv`+44IH z*c}!xv}=U|FCwBehN30mY30;gLs>6rMKN+Y-IARf|2L&3lo~+72mD^qIgNiQ5G&?V zcL=@0&Obbd;uBs-d(S*YGp_}cF39Q27plD^G(Y`uJUhnA%8BnR7YYqfBo(TQ?^oL* zIX*u*XHXZkaYdqLQhEyA6#idl85Bhkg1o5DDN?BvxHGuuspI~p*~24pq|&fP0cGI7 z{z$ub@aVksJOh)qEm=$U*cQ>fWJd)wl&dB^DPH=)F=%N4t1=1ck{W}q?j&QCk%iFb zK;^XT(NF{Fc}kZ<30Ii+%p*wur-iLd=G!^dQ-ZXH2HWD^hXj&8qxMlAV0f zLgA6V8Q4O3`ao(WK{*%10Q1Tc$z^NZIC!P0IO=er&;ME!b^u`t2A3D=0nvG2(QO)z zY;Sh&Mw#PF^+?H-IVH5zPlgXy}`Djj? ziHHrd5i>FVgE9RPe>&9AabbvH$vPCjlQzdz|3=Vcbb|ln>lrb~%LXV0z?a5qS71=^ z($aBz7-N_wT}lkZ3WK-!H`aT0Y60gH4)IIJ2ef15Bh*(*7Ou>{W&VN1*%{?wzPV&cuk=c>ZQb|S+gQ|9dBS^7H>r+=f_nYM>KX_*4OUudg*UuEh>cz=0 z!L+;+yKni7LgU$q^i86DJ}+20UpuHCC#&JNxy$6%Ou<2#suJ>)H>_qT{rMAM7zqM= zw3wZ`riK0(*gT2BxSbC7932H9o_VoS{M^p}hn~Q9gg`o;L}$a|Y6Y!ga)c-4LUuFv z?=>&*TmyA}?^3(W7*DhH!&8(CR(C7XlX|6L9vuGx42gJVSp7rGR?K72Yxt>uFN}r) z0{;K9TA>jBJp<8;O4Eg{s(k3I#_@}IKm34LIuVm4J(AJ=<279}laBTrXO87AiYYnn z2@?RVs^vxhqLN@R$p|f_AMKkez8%jJ-AL!+dCdLmZcTzZKO}WznxDNU$0`j5((lgP zeE`az>M@u!Z+aQCvfR(_9WGx_ViqMR!*SD_4_iH&W(VUa*i_mZODe11u8R9T^5T#> zq-FX!qcoS-e2cf+i@09VK`CW4z=o6y_*F^0$(^yH=4J+ohj#TH7%&M3Wbr~94>pW|l zZ}k7dV4RsYCodU$TmZhwx>i!bk0e zf|e?S*J}O0sjWQWETEWX~3X9R=?kdYBS42D$9j$eIC{uL-dt{c5r8JOmN9c z&kt%p@3t69IkfFUIHnxVLBR7gP&{xFDzP~VJ(w*D*#GPKuRO90GIB+n3M*duE^)69 zcCi#X+Iz1L?OCA^HYC(Q3S)Pd>nhwsruaRM+ zmAe*ttDpm0$@#;X8C=MI~46k?>3X10%`}%ZaB*6 z-pHHY-IQAoVupL?pf5dH2Oj4kV}Wi=URwfO;eNDbBNSMj+prdXyoJ(NRPj6ng972U zhYn+E<&myH7c*5(A~DYfi$OXggb(fZm~zi+Yw~)!4aRM_jLlwMBWc*b{rfkN_6NQV z3HHupj2C3hjPq}Hkj`$dImI8uc2H&=#FkJi)oHlqbT z1!`D^mlZbx5(fKz@26uMqu06LH+7tdSWY<{=^UP_vBm{My`Hi1TLzn1P?;w@THMd~ z5F8mUAr%ip|AZR2rH>0+65S4vgdr5pghP5Bpa3AC8AB2<0usP%z9?;a!N%g<;bR3} zZS^n?KQ#|F1wiu9D__00QG0aWE_nGsmwu+~9YybRG=z-F!&iut6#%_&YA}}RA~S0? zC4iJP^NBm!6eW**Y6J27JUsVN*A&~G*0heGcwnCD(#_bHUZVm`;M6kyqWnt1)0f_D z6?dIHAcl)j|Lf>bGZvtCJiyDH$8(Ac0(Kbx#Q>9OImLb0EXeEkERuNJ&{v{E^4q>z zV;P(0PA{um4ZPWqCT*E1M?sF72fNgf zwdJV0dgS9&XnvJ$GIgDmx8D1k44j4D-~WhNx~RsBVc|21D8?DQvuZdf_n1D__(6Y$ zz#ts8*=}&C!tn>|AkwR<6AMv}iqMUJ%?fTk2(;rC>{RPcHExP~00f~ezo1-`K^?Ri zb=Ep$?1Z+OnlWj~%=aNpj(V#63&h4b$aG^@rFOTX6E?4 z<;8$J%^DLJc~EXiQAynL%i$7zJax?;e3PLZP0$QzsQ-6PMzt%>DQU2n<^`; z7$++%>lB2hc#RD26TrX4Y{V}F4(k0kXxZPve_qiK#QJ*ky%T|czT`i!H-wgy)w z3xMnb)FEXBQAgX#!1B6QM&O}gIY{p6lGul?+vdNi1!4LOaE!&~kP;=$P zxP+E}aorMhd^HycxMZv(yS1PO28%kz_ws7yvm6(yh*%F-u@Mcm!?lCk#bJ)8XotXO zVT}sCG23p+s3G#8@`jdrCn3EGyF(Ebi8li5_YK|pGG|#y5jSAg(N<9RTaFI4`cTlO zo>(8glL#V)&NNlgmJ=?HeECE68dk5t76YaEPKx+@Flax9oaL47#85hPAa0;tvN{;e znm^&RG9Oo!(DOT5;CMIj)LAOk<8yW$?jz08K?}^v2sw{_Xx29#-j1oKosu%{_EaDF zWl0mOURF8W-*XldX-x=Gfy)JuK+Kp+P054wUo_4ihZSqPn)5sA#Zfn^S1)^rHR7p% zzcmxgEtZR-Q5}_%N9JK5R7{`3T5Ga;B!47W!`7)IFQzp`(;?@(jDV_gTiU1PZt{sV zG}Qg>@~x+}o0=bzilLFN45bI~k5hHZbGkGM$~#Q6gGDc(1dlg)vNr<OF9(6wna6R~Sz`JN+xwqw58ge~0DLjfU}(9P)<0s_)iIWo2@ZFr*;LrFNt zOp2$7waE+Wi+O8cIL-~YEP%?R?%#W~7e0{0va%WHK=)J8Zl+GM?Fyk{S#`SmDwB;1nX z8fjCM;tb|uVM#coig_82T&rJtYp}7Y3g$5C`OO)Wh@mqEdQE6qo26eYKatNq*rRan z1A2NxRYC)0&ekC3xydc(&J(Kbj^LR*1ng&m+kT-EJ*%GyBT-dUX6+8R<{p>b#wg7U zj?-M~*BN33-u$^JW`Cr}e@kw287A%&Kg9gAM%3fQZ-rOjdxXyF^`I;LD&K&!5aas( zJAXRg6S4TrZ~FJ#c4R{Plf8cQjI?l6HRSg+1)ov5&&ikvML@+-j`z>Pds9YuY#vVt z-mk>7x{ciG;yXAUpnSsp67dEt~^UplyC( z0a{;T0$#A~Ywv#C{4v|3;)f^iuLhI-U9z?NU$^bVk_Ne!a~u&<2o(Hm>JnLUD_Y5k zhXU4{X8%$Yi4PBGb15#*aN`6!0>ojr(>HZ`M6RE29+rVMmqw;xl|x6U6pa(w=UCIN zZE(>YzPy!s*h?!9C`$j#GUkbNba3($^jtdlby47Ko?Tx_wVf^@4kXoqY40;p+b_wP zWlRV*3&5=xCeo#DlYhi{bO!L@$~%IqEOk zlAVLoa#zOOqcqo2{rI#1(eOwRGZXaldp}Xkq%0&goQfu+f@+%1lEKU^jy^k&JF#o; z24O{m@6FAu0E@OvNb-mcn0X`rP6XgUBGe`Egs98`4V;PN<|#^_DBQyoJlpqacwO~g7XH3dq&+1)M~xQ$7S1|M?460H-tnwi@GCQ=)~ zHW21q|?f>px`VMC8==R;JHy#EIi`Q4L(q|c7}e{B^Qs2=0WwM=Tti{b)p^ z;d}qFv0Ybz20MV`MRLs7H0_Xv?E1GkCvh*t_YuyqHzc^9Ei6nqcTrx-9^duMr^cVl$&u0cNSEIt1vOOD6Dgbzmd< zKFCq~R9mH11m7SKOfyj{)Cl5d&VU&dM}tO8+)D+X&gfX1E1?xYNO`5;sc8WeHEOwf zC&Yr4@7pAzz)OwbGOhb2S*?`+!!YQc(>lAs+-=})X`ZP25gRBkN6r+PJ})FFfwikx z8U51mK%`o=-A_shl|4NqAGA7EtwBsr3-6BE&>}$pxzvsE*V^}S_HM2H-JNirQGK@K z0&j4jStg~&Mab%@EKUfmxUvrBXSDQb-Ruw=!Q{qB5E;m0C5Wf$znfd01L|1?r~9|8 z^n#;Go~(lkX!|z@d_)1rMVeI2uJl*$n)ci_hNiDMHX7|eTLoDM`Uzxep5hD3rL zQkGgjc+~I>;~^ivQ*D0&sF%k>6B+6=75B$wwZKjnrKJQswQDtc65f@weUC_w-)Fe% zpoXnDKtd7%IEtvQxqMjKMC1I8HSeSB|4gFSfCg6Cdz7Mhm=-CRH%#nXoDV3_)6U&? zAA(m9a4Y1!IDh?6)hw-tj8Tu|mVx^qan~K&>3wV)7b(Bj2t$Z`#jaeB)ajj~Ii1Br z5=tSmppmYTaptnr&OLh{eP#Rvl`l@(=zXVCi}AP>3I#E2wt}FR+MPX|_a1wO zRPVxw;ASJ-qh^^V=T05~H7rlPwAg1I%+Jw8`A)||fKeO1M3nT+^WN(xXn7Z-U?f4n zt?#AOI+R%_*7~48pO@UbAq!YX3=ECSz8{$k|I}s9WmaWT5O4CDfIE+jts|m5O3OY2 z!9t2%tX*~!kEINK{(EH{%=~DJ^r)EQ>^p_7BipvU?o{`rMLprxcPv3K{e?T%^|2ID z9`>8>iLaj#!jkeq7~4+@?%j`@WwgzZtYRO_+V>;7tUPbqg!ntCd!1h`K+^AI6k@F1 z#T{A^M^pG&*Ze0q>Y#H1cl><8^@h`o=SpP_8KPNR8G2GBe^m|nf^KjOeT~dD#E&ay zD8JlLvx~psrlhdHUJVF;!aQ3RR24CH+W5u3ZAckEe=Rmrx6;nk2?Iix3}PeOUT`y- z>6)6tL_#z=Ld)}Bpp*h6kTter%lS1YY2;)0JEDo(g32%|3)^=8>o6|CMt4**M=2nX zN9SJ6vt!(HL9gmWop{9XTfTf6^T1PtPX^k(vA`R6`MTar+XPP7KHp40GP^iM$F9mY z@L1XG(omEPRB$IpYRwI*Afj_&w7Y|CV#(A1cj3-0nJL_w`_EmOvzX=%=Go^}jfnWk zUvxJ4At?Q>De-6d$9A`*E#wba?!v90Yq(myshuy&ZDp^o|Aa8}CNe;s21>4(`T)aF zo=7Tls|WbS|4OB5qpTAj&NSf1PzS^2on2m2nL6<+?QHhP+L>j~DF8(0T{;yMPC`|^ zl0TB93iT3!WxECPP{B$q9L$KuW@KIu*nA6GlmMuTHChr^mOM3Aw_UqjiG{ zzY;%I9U-SQ_N4yrpjp%-n-IE{xQcZ}H|LVIk*d1fT)jXC4#n6;LSa$J3EH`TY#9&R zll^4YbQ~`ydR#0F2H-~;zC?)mU+|Q!SE496E!`IJu9I25i}w%}Nw~898NjN!O-XZy zED06buKYoIZq}@4D^&^KOag?#yv%g$v!RJox1(6+-yDu z?Bln8_bR~YKSrOD3Ln7glm}Jb5m{OEz0J6ciW>CG0 z2^M}GC6Q)pMu}hjJmM55IMlX*6K}&_hO1Jt%h7@QNvnL8JQY==ura@kmK9KMBLgM2 zOJvXmI%t3h#Z!J3EDx^3#w6Yw_}~^V9{(S-tctvWmi3~s>j&8>?PaUrq!tzafw}(i z($CZ`AUA5J-o9HhVh&Pcg(SYh&0;5)1|=-Sb^8%PkV5;Y7|)0gV|oi8o>VTxa#11O z(RQZFX#eFfPcw;rNgmLm5NPhLRjaxdOuF3~-ogXh`g7Nxo;0$1stpHBnle}6gt>JV zj7aycsQduu1g9zrQY1zE1E2D46vk{>Fteb6sM3=+1ru=pET;h*eRcmfC+PgmAlqjI z=(67F%&5bRq8^H)Uz1Il9O*2|XZ~xM=_^$Esjt?hH*phc0ux_#@3!lfV&02*iv2o;$Jik(DCt$yF}UJPl21DF|ck4(3rhngoQSm zB*MgFsg5kW2|nY9>cqPP*ZSh~sA~dRTo(@&lI0V>hR*3xvWDqSIE^^f;bgkRw#Cg7 z^O0niEvf{#(Ykkgq9uG%fdRfg9>OXJEcK%1PQOXc73k6U)VwBZ8OC5xzlD40kQ+^P z%sNVo7xB}^p|CT1AHG*~8TgEeiys2@bqmt(PUI0-L{60myDaGM`@RNb_BPpmA7>4z z(g=q6`$W0`NT*SoT-Xe3+(I8U|Cku@#RmykmK449PT5L(vQ#|qe*VQ38jq_G!A_;q z;pXNoHP98poiiFc?`U;gqcWSI2t&WXy)PXpQC9u9T2mBXYS_gQWDxiXie`y=z6|u- z?RRFvY45XL2LO%Umesu6udoyS4A7v(z=b^w)f_*PhA}&W5&@GAzt`*hOuMQeFS24G zX}Pao%kke-NV$Al{Zf6?6<>5I|1X3@&5TMP+Zl~0X+58d`tM90+Y4`K-*Y0cBCkC* zT(bCCfPkfp+>MkjkAZ$MeWdS=YidtcQ4 zW-af`;MQm5T((zm7Dz{k3idr7uZ&Ym1F* zKyc;pCRTrm_?5cRkp>s~^VO#VA>_2`fNZ8(>unnv!TQzr5rY{bazMzJ=B!>_#oldN zX!My5kq<{D{HK+nG_z@BWp>SzP=9RM+z)F!(p+_x-cwGtDqt|}d(XJ-mFQ;=OCQ~k zIOTe9?+9<<0?1Z^$KGmJU7V9oDW=xq#YS6*wedMke9eAgpyAWm?K>?OyS?u~|GOso zub6X~Mk+s;ob+gVUo8$xT!QG{Py>|4Z=Zh%Ma$|^jeH4Fve#fb6RpEY!@UdbhSS`k zJrspyF+40_Pw=FI&#*EYMP=wQX89Pi&DB$Z->bfd?n368au%(? z*>(O=O~4P82`vDMi7%Ps@bVw=g?$WDUU17CnSHb%;QoBn=o0#qny+|K;R_Ua*EG!{ zx@hMP%0E@;E0{WU=nP{(lek}9p-)b+9o>brc7C6p9Fb46wnIYYeeg2E)qcAU>qHs6 zzTR`~%5vi+i&wwx7~`3N4j+DmITfNDj&CONW^9=FiI!Xft2oZuK1A9}lHvxeVAKIO_Fo~%49 zWw>L}a(3aa2lS!dd)nyrhDzFr`E3zgJ);!m&Wbf-j{d> zNtt`^emtQ4l-AVPZ&d1w#RKit+yDna_`lSe3Broe=J&pdG`Pt${Wzy+ zM`ZvBtyh$B{h&YNakk#Nc+0=ion5uzHm}dX9Lsm({RSwpn)k7{pG1~xCl6kql8C+u zP@^PEhbevUwFmD$Ervp#>vG?h$6>%cLR*TRmv-H;Q_qk?ya&Kt(6(~=?yl>nAw)Z^ zZb0nQH_TLhqk<-z?_#)_NVRJ9!mTEC;fq-Wed%iAiTx}%ax#H((45UjQ^m+yj+&6| z*%)dBhD#+~+%0 z9ZDiEg0|h8aimjG%0ODTpSmrvYUFZsPkyzcWvlLP!^C&j@E+#hY24${Ic9qQ6w@K; zOxvDkzB1#}5WGLnBKZEYq->aUp(4^TVT|YbxjLYbiW(J$n1xn;;Y>NHm{}Ez7!78| z9WTY#I-Iz#*D~fkWhI!<#=XzW{cb}a5x-4ksHxIYLv(ceoyD5Hik<0nhD|O)&|BO2 zOhUFrJl+d|E7m3UrljJ=3Vo zMnv9Hsxt0p?FV5Wo~_)UFAs`V0EVDBicZWr*lqQ`0IXaSfpzDJJHbZ~{8|NMQ2Bb_ zp7m$MCq~x^A~00A5=g>ZR8PMR3JBbUWjdu4nnWqi>ds|pQ@O-&peDbwpK znOa3)QCE3xIHceiflS9A&HWJ!D9emof z0IaE5^Sq^gOl(}Twp9!z&nopKWA?XhD34Xl(aJdBzrhrEQDMv65_yz6=URX{Dqi}! za)UsGu0qu8=cs^cP`rhJ&l|~kR))Ldf(w@a_W5kD_tPZ6?NJes1-ZW7{1;%>?zn?_ zgm6GnDNws41#0V>z*KyE_HKHp7)53INfB+*&$%DyT#luGACv$zsJRI%I{nNJRmHSI@Cf)Xd|c%BnK*tf7{A=E!P#0shuJ6?NSvB zAFspVTkp6Oi{*V$LPp%KPOKUlB|IEVxs{yLzO6rZU=Z3OS>9eecu-9(WA7<6Q5TJVk3Pn>k<9++RO22*1A>f! z#(5wkqIS{%vx14yTF@t)3J$MNpe*>HU^N@Gc(Yh8&dEEr%*?mp(*>JdRvGWKB9@dw zbL+#%K-7N7>`eOc|JF8I&P)Bkq1!J2BY&n=W5V0?H3x=7EoD`ZSYi^Qbk)#SX`_J( zt4=%#@nQ~2pvpQySZSSTL1qMu1hmQ)zzSAjgkrHNIII!<%})~q+PbS**M@pXyUX58 zb7CHRl)2BXM}lBE^xhfY>$-%*pyw9+DE*9Vc@6y4dIrZks=GuVfzKEN1{Kn z>4e2|=ctbms|m1I5X#vKtmC7=%5n{)R~E36D6##LYeZ|6QZZ%Ymnz0+FGX=y#c?+C z24`;qb#QxpbMfVDv=Nj+(g5rdaR+5)a5|r23&QKumAF9W}n^~z~j&RKS>c- zB-gJMqk-ThnsyYfIy=AXLD$bH5l2+Ae;O1JOr3iIv2M#vVr1omL~wvATv<+;!WsrG z`N}}guukc*`^Dc$NUf1HB5(E?{LEuPhltI~eN;${2=kW0ByZY2QQVjK(|;=gZB&HI z;M16{A0Z}6wSyaI+RQE&b=0f*d4Tp&k={vk@4OvGpzPKD5!ZxgvHliz8?Lkv7wn*~ zCg}}sg=zdPi19yMGS&v>LCDNl5$yus$0E1Xisi0s907&x?D35b0*#2= zIZf!%JIdU4XbNzkA2KG>zz06KN1mM$wfTHX42=9QvXMF@-L zbv4D24-o{GDGu$SdzKi_lfb&SW)^Ux4;+-l6om~$X`WAJ3449}%09f}u4IoMVG5l( zLfU2PH>Btn+DXviwN_L}W+-um>vFA*h;m7Hi%L1A$J%42h3V}UC?z=O0 z1%xARMh6Q{1_~H1yHGWZUG%n^t{IsRXj*l)UpUiCyG%zne3CNQ{}&yrVS`2ME-G0n zN6-*1tFqk1sqo#x>-JAC6k;Ip-DscaE^(s}{^fLWA<46yWS-(9)}dh9?cj2y%`Bt~#lv_=#H6@to-ar_ zUL#SD68VkQVc|9Z@X9VZIr-LTxzP{oxta&spVIx1c=Ak>tu1{dxCDU$^u0F0TNzck z6XU219r4(AW3H4=-1UMQ#pK30?`pxeNj{^hh^Ta7O8XlTe^|FoCo;~VdOhb2n@(Tk z5A;C!Dm>|DjP6h?LuB0XX)qE;>ZV^p3O&g%)%NSDz+>pi@N_R+*md7bb3iz}cfdv+ z)gli5f@LtqOh6WAUpN7pci%ZNZw>_N_ZF*o*}1gsgZ^yr6BEAtLBHPWnWoxYDe5i` zmze1-k?`je_QgD7oF3-*iJa2#i@qh4v(1lWDbg?Uw3H;A@DpeB(fX5oJ0v)>IBI^M zrV5dabx1VhnkDJKEt<&!JP&(&+yjv?qp!&!2r`LuTwnvQfA=4_8h19Cz}w zib_#?nE7CU*2DJ1QHG_{b;u4n!t1-&g3(XzaeHOs`SR`U$t|4lUgJ88PqFnO<(4Zo z8J26RXZbXdzG=0Bo!gH3-)PY5-ZHA`)fOnt>Mf@1iN33Nyv=^YZFQ5Hk8@pbNMVV( z7yK1qfh-efOM$A2)}3>^C~&on3{Ai?DBFc;yTmCH-d8GyGXq45bU=sDOVw%QK#Bpy zRxZNrPn%gp;ZcU=o1VOWkjEGKSf}o0-18D$dnWg~&vBK-0N6QBB%8$t$EDt=3%C~Q=-C1nA|ykQw*1h> zld=Bdi1qMR6G;W|LDWgg)7!gz2_tS#)FJVah_!Sb24P9HLmC%neSPPyer zl-<5kF>8XQ@P}QDK5_)U=d(wYQkzomq}J8ny=<1UI>TM|7XId;=0p2Ffx1_mUPWJj zo~RHc2p>=6>}Aqp^yl|%h>b3BBQ~GrbIbPfa*PV;Vo(d8e76-3s zCXKCPHJW}nQ#hJC@IUP9y724g*HG}cwm2z46 z6=X-ywBL4F(wogyY3riQQ4STz{%%||VKicL2oKq(c*MjD&YxhmMcF~oU6S$%YRc*t zod&LRfM=O z9_M<^=;CXWKlseHztF8klU9IB1Sq&82@B622NLAV%AP)3tD9!dh@EIOjkDf_MV^Z+ zGDuTHO9r-)0*mV+`yLjqj?BqsJ@vj>a&oKmPfin<7BU9x0r0>@14H_XMioV$hJ78KT5MUqDxE;v zR!qbA;}PUT%m^WKpGnK73vc-{zMtaGu>n zL}}d(S!wGQ&GghsSX(gRB}Q9zJzIbkW%~vQ{IOS0T85H`LX{DM`7z3~aVg=9PnSPB zfi4U5p>Wj&<9ZaU9vsFD!rkc)Sw zNO9QaLc|ON=y;ZQNEp=0yRcfK^^M-QS*&h#%ECww3an?I7UkP5-qmBGs8q-x{bOl& zmXJyTKH8d2v{*4D6}_NtFNBr}4_#<(WvBb=O*TQw+*F>>VkwP^W*pSh2w$2VIUw0?5vg~CHE!^`)vhT*#;cWQS`!<&FDzT*|;rLlUXmzl80vjDLZ+QzFnUSaJoSq8y2r@OJg8QIUq&Hcgk zTjSxV*Kf=g@t9A1fZJiff1GU&C7hqcM(Ad#JxlyToiTa?tB`f~A{^b)&V&54*Ey=C zS*Kq&VajElKts^8eZS#sv68pX@x4mzPxRO#=kMidyT~>?pc&=eevA9yZ%4M^dLcXF zg<~)18F#?X5aKB~^)8a>-mKe?Fm53Wv6gK`7C&1X90pjqjq6c_SjZtBqY7WN7L|;% zCRDi!KGeZTPxWfbL@dOZy(eA8rtu9* z2pL+B5(EK0F=UyZ5GeyZl1LgJ zrN31#sokH>rd`omAr$Q<1Vqd{=*-bN<1^;LE+rKug?hnnmv;*roHI6qL}!k9Mz*o4 z6FzCO@JlDoNlX#IlnpAjqb6CXS0m0on{iGj%JX&3HDRa`XQNnFhLmUhSqfzB$x9^o zPMS+(w-m0UbwtnjkUqe{I`;WgV3qE-IQGAS>-yxme$1@xgnF4zXsL_>W~M%WOWV#U zq$>dX*1L)Mn;`8E4Wl!dFRo^`SIoW#fV&2-UYx16%!OO;I}4)yojRv8v+ZKsg z_2URo6H;*_W<0La&F}a^hghpno>@tBqY2s^@e_NXn^wLIF@7eHe^zTt+#DcK85V)(m2#B7;ZD3n$+Y z#qRd}X^k`3`SO?p3*4d}rN~CG?p*Urb&zNPAh4SvVtq>KMKT6X>1@TJ&K6^~sD1yz zL_p>nIy);DXmX{sMP_L3*Twl231)u3j?KS>Rj^xfRN?D$G9w$bSa?CDkBt!XWSQU4 z6rLYsbn#qMel0_ZbvZwh=jt<6k!e`Xdjynt>}HXs^5^W3k-B)Buu8a8y?T3__BVmG zhiaQ_NGiNXGFbRu0pI5Uh39J31WEOyD2rDf7GK0-5R0(7yywQtkC$V+*G1ijA*f=oyhU40?&KF@GmEf!-mp#-A*I?H7Mc%C&7!t& zyvi67fa6;NF88V-bUn*r3~q(x;2%QTUrE^fjYIqydf5hmevM>ko|+& zso+hD+(vP&z}i%+!~EZ(u6<00u{qN9g@Q%xiRX={E${AyPesqhnbz{`tq+358n2WK@+^EheG_jU;@;&HId&%?)r$I^9JR4>DSUO5 zXnx*7epzO1d7A5|bYSiV?c~Bh6ZGzicn;X=E{|GsZgK~$Gi#$1!zY&7^F3$9mz_Lr zN3q>>hz}&kBVyEwknO$kU0=YpxM1knsM_izBZgwdgqL5n6xGY;gj^K>p25v48+ag} z@IwQ4;6oG5OyCY73G@FTryp_lz%u*&f&37V-2Zs;)&+z*kX7}N!UoXoMtid@?l0&9 z<3%^HNc6~OAKI!yy!TavMSXtJu&%HAa|-6y)STWDpKaXHO%NcuyLK4sLty z9jF>+AT;Ymnp!4eE+kBgx>V0EH=F)HMHbN7O2|zZ7jy*z_Zgpi0F1pA>7eE^HI{ zTy+$NTnhHPj#&TUO4J|D6J{amM zWdx;TNv{wQW|1dc4b*s%QCw zx8m9>anD=lZ>{@frj+)F`gE)0hu#klDpxf!-^@??ijNqZ6e6VoHX#p?*Z+H6;cUBb zdo#FjiJII(!wciElPYW8cr^D8D?eOs7DG01{6>Vn(F3c)mKa&rs;@Sh^zc#HZp05wMd+fX zU1CNwI1qY|kvb{(Eoo2#Gq;&Wt{-NHwoUDWB_-v#DlSOwPp&;;P|D_7Rz3)86hpT^ zQb(}qm8>=rD<9~5@gDuZ{8qkQ{8H1{lTZ4RAt#MZ>v3xV@@Dqz=(yb3$)~IE-TolS zGNNWrlfl~B#MJ$R74Ibj*ZAm_G@Wv`g$0i5%}Sv>25-c{V*L%?igNRXpFZynYtrkH z`BA>e4ophS^c{MPn(8KFWafIXf}mpq)lLM)u?Ex9E#V`Q4EKtC6j&E4LBOC|X_#N! zb*Zb>1gtbk;R?Oj{L7vNia@_p~`E;%%i(WbL(sXI}WoB3}f0LijJsqSirx4GoUJT<|@bEX7 zcQvF%j^VUFU`ZM^oKVSk4wj{V&!$VEe7%9qHY0lPomBhBYatq zBB<#u6|+>@8b9?^=t0SKgQ-u>iKFFX#i0xzBdOyVz+NbRCM`K?p(3)p|NvN0hEj6Zyc7o%ZAb z%Y|%EkW%YPfc4EEjvqKC>?cZ}aft@B95tn>ce)adk5Gb);JdhueQ_jQ065;qP zKLMQjP~Ky*=sMlebu+kj6fn;33_Mb!xL=O5|66)HDpo@HM*KggCK z9bb#*{Se&V1?B-2`V`!&cgN8%;lJfy!2JM_rblGn0d_bv$#+=^g-R3TAv|+u% z?YK^WO*_gMfpCRh0et;Ilnb|ECQ8Rqr7hn8l3$5VO5^atyhZ3^k>c&^LR*O1kuJU1BPKSdG|hE%={k zfVAxN{&c4{y+k31{G`xKxr#J;gkF$j^qN3xrxovfE#{lIF)7=ZM*FT_k7I&@Qm+vw z)k}~D&yZ4Hh@!jHRy$~OF?x2NnZDxcZR=GPq36wu5ye8w%`2h}s7C%?j+a(7S_XCoWeNSF(nOTx#iq9^OL`=dam*y}V3YsBY9o3DI^ zLKTLHgdJJ%!hp~{2ui19H*|#;b7&M5!roJdC=g0R#vn=L{@dpVr~x3CWMEj)F8J$Q zX^2=GZa2h1fSIj9&Qmyw{xOvzYX4{;M)2LffdnKy%CzH<_KLQO7@A_V^#|@RnWqv%?%q& zXr>rPuzDPcF!#pr$k7XF4JB6xHZm^l55ZY?<(RLHOUTXJVAgNcEr;JLK;PHZo{ae> zD|lb4Ix)$|X4A+TKP3U0#r|wx4yF7T3f;KXqPW2W3_dadT`Bw8e)~4`h@0DgzdlfE z!PFRqflg<^fe{i%jSLI(z?`nSkgJWQy|S);;FYK zI2DSnas1g#RJ<*!f~jlP3B>Sjrc7uUibh{I_BRMz<>EEYT%+^w4w$d-@VXo3<@FP_xsk066aVrt2B5=XGN=f)ebXU)O~EU{%?HoiRUaz7Mdll5eK z2btRgtMEq?33>=_G#8(a9cGRODO257D6=Kw6UH zIC2^94rPZwry(rkAt7LG5sarYN^V+Y3Yxp4F&mM9iXwzZyIC@yTXQ`p-*z_SvOW-@ z&LPI#e+3{+{z1fm*AyA z|Ld45zGZmSxR*YM#bSv8Y8Stv{~&BG1hJZv5TNQhezC;x&7v?BAnz-#hNc)e7YZeK zisLw(a2qUxz)NI1SGGbv!mHCyS8}EG(kTa1Vs$z^aF%&6jHgJlk7|-o8geOyzKX`z ztA1+%n|c3-XG!X7^Mygu_BMXb-(hA=)3hT?0L9@4@aj6+?MwHRY0na-%aJKx&UK}Q z_EIvi)R)V$V(1t*s-Fb3Mf4bphr4u6Rg9201^RT49BcJJnxQyZX-9=wR9m2Qoki&=TM1*x^t{pg86Cg>hODq@ z-xj47&*(HiUBf;u-hH{ZVzf8IXbbM%f&lI_ZK#e}972z6(0!En8pMA2@&eONRQIr; zkElx6F^Sd8xRtEGc0NbJscS>-;X#B2BL zVaV!wiynje+3VSIAB#gp^!_@X#)jwG+3N$AKZ%NLmIc@t!t=n*?EIPhyV#D1D6+e( zkK@~hKG~%tuu}FNY?HJqVOSL)TsndCclKBL?&7ggVJ1HfzHu}eXR~Rq`nug5zJ}&`IidE5 zmRawY%GIiE_7?h?T0pE&ZpVCVNBIU1l*_*I46IPU>K`L-{sOjiJ_ts0uW)O|tsT_> zU8BA^2$Ob2;Y~Pa;nHPXm>%6_Uoz#{yT|1xr$#4EEy_mnU8l?M9#*@!cvyUU2t^6L z!HgpdZfca+vW;h-{)Gbicjefu1`qH3esNaUQ zH$=NMJRprNXmS&gRr8G-fC65p`Z?%?6Ut>=5H9d0KLnnMeI^N=>2*FY=7BHBz%}$1 z#@Kg772;MxDQG%Ndx`O;D-jclP`SU2_h%s#rsV%lZgotjx!s&ai;w0(+pDZtlTgYj z*#o&2FB4t5(@`&~LIb60HQK)wva~@yWuKe|-SnV!cj$FTA|<})?;8w}jSi%tG(j0) z7eq#9t0k2_1JF%VwJL4Ef25ynidRW&{i9a&(lO&oUEdRazW^{m1-4lBXiFL$*#h)4A8J~&9=6Gp%a(EHGaOR_X83wvAe{JUo_S7M?6xW3QTC=z>vFG?Wt~wVkSbqQUA!osxz1bNCzzoCUY> zZ;ry-8$ndm=<>Lw%-_uF7dhm+uPSQ;D5{dBnbV8)c@AU}Qf*wqE~&n8^3Bb7bJ;-v zYja%fqD7UYSw#+fOgxKoTXyu01S&H2p4XwYG%vfB$L1FijZKPR35NB|rc1>xkG8T| zJhA;ug^6FCeuhj_!!?{?HFpOnxuiJ`-ojLw zp%HlAx|yRMCN^hOW!+Tox_fGloaLQ&&sDE}D^?GK&>pCehYmC=gTG;U5!81d@z&{@ zd<1g4a|Dl!?!Ovp3gh+DnWQ~_c8^)(wfwr_x#pyBNXdY8{A z{&K?Td}kb0P1G@Ovaa0T#V;5_p#>JmyHebz2o0h(cPiNs;X06Y0N(9#BEY&{oG`mf zaI=aF3rO>JAU?o=HnFy_BYUsJ+raVQI2nU=tKCxO7hgyBG5rs**Kkth_vl!M|0Ib- zvuyeeloP4)-~H8QmHNrYH^qh7Zc~fKFf(U!C0A{HD*b=Lgtc_Kn0`U)lzyVcFzc+|AD&mHzSL-*zZnMHpr(<- zvcKMaLMTlhk6@_$uYU*!*LI~6E*k4a08D9fpMd#E;2j_bi&Ojsew-eS8pug>1zrD9Dji>ETS z3%0;Xx2{;eS`$dH)5y8A<>o0+j5=HvGjXf&g2A>!@Uer8LE&}FdMVM8ywvdjRbCVL z!XS>z>1yl~_>xD?&J(@%8BRkwJ(4VENB}emKhi*k+zXeD-COyVk|=W;8M2cl@rZ|U z@IS2PUp9O5QYSKch|)*mrY7e@`itOsP6kI7XgJIlx<-6I65aa-j`N2nrGg}XJb0-cab#;#W?@-ep2v##EfV^ zTYwBga9nX&2|C0uB>Uj}GTvBRD-enDf2mkLh$VOC(D< zM2aN~xXH3Mw3>G`r*4~bPTk*tkrJ_aUIxn2poLo~o59QB5ebHZ_<}x)uueKk3f^n} zGzKR8?+7!$n}FdT*jMGWy2ps_p;-5;OG zYsKq!<=%EB@7B>Pn9k9!SHL*ZR~g3VnZ6R%8-oh*LwF5KX+@f81JlP5t;Y{R_j<95 zc{(708IZvH-*JP~fwW&w3Wv+p%xnMLm>H8U69ba=zc5SwzNK9XYqP2{;+EF1Mc)!< zI2?#k50+RP&=4`MM@Z5j6tlVosl^7jW-jy7Nr5f|05cWeAkZ>73O*@!Nha`T?S$NP=DB;s)+dwB z^fk$_p?h44C@K&RiOtIrHNK-%=XeAnRdu6d2hSGGr%k3LLu(-;<&!!BM_#3X$bPMa z+IY*YoF^mGEI2?9kcPoh#i%r2HtUa0K>gdiYg@N#h5-E`BybGXML0YH0lr?6xlyWLMg^|) zZdN`;FUys7IobWrdh=KsjU#g^;4W?yb!YLOPaK7d@p1n%?VKqhW;yN@&_EsqQHT|i z^a_v5j`63T_!NQr2^x&(W`RyHxd!Qa78mo{`m^&6!#QC9-65y3Fl~u)|JY+22=1Wl zM=j?i0k4%&P*mRMcg~cNy8{~GUWecJ*fZ;Vx=oZd?8MFDW=-s*T+jqT316n}EmLdK z6{LpKBZxvQ4!{lM9J&GE2+>;Ug>2v}!$^={w(g8@PV4krDusDK7^APK<)rF>Q}ZmD zr{mB@Cty8!&`VK%pLwnFo{kVavH=zia~=rK097xWqF8Nc^*M3%eGDgauUm1HiaU_p zY{L~J1Mv{03>d7nqm=u6o$kAS9ZH68rZj+F1+s-85Du;if;AY{{vQG^JrT?d3^{gGHCl}fN`mrUR2qG(h6 z-+e6V$RP}wiG;{|J9yi*40wOfXEE)Is$z8iIHdj@p70m#6lThzzHNLZ;Bi`iJN`== zRudQTcCSPx>1_a$4$ngVSq!pC%BK+ij1v!r69cD9(=~0U*;>Ja1Xh>Eo??rFiI@r- z=057(u}`i$E(qE|vVIOW&6#`z`E4$@!>UPVkwVn=_bV~a2yNY*30byUb0FvY+z+qc zQ(Nb12|^Xd*ack@XWs#~c?JWoyJ*8f6Ab`WmViiwH$%+a?)*;4UhvwTGQr7=$g|}i z8WcHPcWC7*BPSA`iM@=MIv!21EycgcB}M9yB2=T4jxT4Uxy+}56%i?*L&7D(UM+06 z7B6tFwfodPoZkMDUW3 zLJj^Odr9?Dr(*jE^|~w*=)@)Ib{dq?62#+mZyYFf)}*gG>RF|GI21%~8<4BppO$l! zc6_=%eD$@ibHbtr0TXCAy<;Q+oc7$jXzqq2bq8&RDp zYwEf+5VokZ7>;MhN0SqCp7t^f>4UaY^FiO)F4lB$dyVAfX7SuN*BWI=v8(s>5`mu> zji%O>2ICz)AZ3#Yc;hEuBvk11X@HnUqzFR&K(Zdpj4F70g>!#dN;P4n|66Thc8PC2 z5$^kG6@Q$nY+N9RS^*Tk!;i%Fa!Ms{CMX-6#7?v~F`kQvN=*ZR-*n+Bak~s2v9znq zz%X_hMDk>x<>?2ZRm$>o$)~RHaEc+Ju&FpJ&J-qtP~8lyz?YA}Xe1ms2#j{3D`mYG zP`_wov)7J5T0X{zZ9X-{7ny+8kjyV9I=nuG!_uoOZ(maOgPR@IZwzE`EcWc~wB8meWl>$E?@z^~kXpKcBD|d*QU+fpm@W?N!I=Ksc zz2LmK%}=^7`wSZ&cOUTbT0*wTl&&Pw8y5n{ZOWTA9P68pddpyT1b~5!>R8n4auG{X=f%h{do$Vggw${ScQkF3bRgG&LPA_4w$^UI~EC z=?1BZti_fFPMY_E(m4ZZQB(*rhB%O4g84Oi&Y!XVR>!Y*^9#VH0YDoPncKHmA$&YH z9q&r+i`^jkq4Kf1C?(0V;Rcg&n}H+*zcvJ+3_LCLi0Qd?NMsY=7Dt~&9{Ydk!)RHr zS*a%fwo@mc3&9N~2nIdCo=GT#fsjQvFOSMh%DF)T7s36&hlA*Dd*m$%B z+mO!!eQ!`cwwh-sqARaDT5>E2dbO=?DgW+`1CU5N!ST|Q>S#wn z5;@#FCBbsLFFwh}iyi7LHVwA_L<@naq0cy7L~riijn))1OC7m=f}*~1%BO@o`ED7m z11!JH4^wvsomC!)1|V#FKR=cpxd#cBAdtqSRiP24`XcZS61hk7yJgmX`IfJe81wCJk6;Po~C|@6kC=;~VxdfOZg%Hi15F3aVa- z&1BrW1cyP?2+Ep~n2jTE2&M3tO$+tudAn`bB3G4rlmuf9<6HN)Strp0GI%eXFyJmd z`|M|?HXGz%jExhW%c82$aHO?s1J7<{V9?Uw5IS5=KQUrN!U!={M0LeBTs=W*2>t-s#%CS%(nSXAA!$>U^7yx-Z|1xg zJ85|S7N>NLncuRWXR3cC!lqg9H}6dKqd5?Z_=TAN&=UC@6o9e=vrb)DX%f&UY$Wu$YqA?ITQ&}CrMl>Sc&+A z>+NX_OdF6!2oTMCO}aXRd``Q!q5(ls+9iR{eeD>7vfs_~{zW2pi! z{^+es6udxUo^}!CT=a*2NPy9MK7DI2B4w5@h3wo;MV zB!_B2{Iy@ttMP;P%GOdLzKtIG?nzl0&`RF#-h!PwSoP#z;7jS!yv~0kg<%WWM!+L?NBWW;7nzGDV@%Y*hnrr}RL z-QNoO)*gnv?c{m0c!}3=rb$(s2OIKOG+Zo$!89!GHS7=Gbrq^ZfBK$dDNycbAC1d@ zvM)ADAlDTN(F&>9gp?|KVt&BEWd~SqENuD4J9pjVZy<7sk)qUn%dCbw`pTn(6O@es zwIv)fo>d0U_AfYoNQ+7@^*}?5hc7tI`-<~`DXPT9QxxLtEF=rI244*_2BWs2JAae5 zyZMFUP+%}#uD=h|@enTyg&k4vbpeO2^7cXZb>PG-Tbxf21aOzJLP>qX-3Ex3_gRrf zz_p@U5Z)trrTUf{%l)>20k1>~ZM3>Is?Qa@fbIF(zatDN5gJ1$fE>@Hsv&( z4I8~^1O!AvOE3;=OfoFQPgHyineYeABL^{|AP+eh)tXD;@nHU2anj`E0e^AwMq73w z!fP$13l)hNe0k)K(=Cibv(aDckQ8n+qkrBNIj_JaP)RD=A!^b+2s9q zLtv(6UGa>7kJ#sV(EL97;2N(j_!E9=%U5mYuJ(vb;7nH7=J6_=Hi8TkHItGvxN3Mn zMPdSc;fz+^85PjJ0$ZJq18aC6Sosn|yhl#Y4JpxXvAmTQxS2FNEWdT9{o-?f1>hro zlwm{9zflqsSvoh^8TT@4Rb2i}_TdPKIN{ZhIayxB(_Z0DF-CrsV^F+`6B_1j^AXe( zzGJtx9y-^Ms1W%23R(aOJ7w6NjT*z6ZafXWaEsBQNtZAfdjR+5fe@mePFW{r{R-Rx z;C8Hs7u(9nI3>#|foCdB7Yu_hdl%AD3_L@wHLg-9Vb|=*8Y2yG~oAQ6wEx;`cpHm_`$nwNbIa zhN9ic!6BjiHF*jTI}B^sCQKXF=iwIGO07<0Jt4I@vA-!rGcc*U@`}RI{RbUZaR5n{ zYozf60Y~lMV0)%A4X`UOX2Gh~+f+^jZOxPrEA;`tX49VN|U4s z?h_ZO+UNQ{MhOUK73s6mC=6Vwfq6&&^8@1`3G^d6b|31UudnJ?YzY-HK0FWPY6MZ~cE)3~@e#?p9>7-8RX{QMrX)KMB2>~Vz}EiUH^B0y=H z?i^z%C#}Ip-AKUwdZFrvhAYTYT>nz;&&^o9s@E=xmWasO0(Pw0)`mjB;@zeiVg*4( zdzR4>f*hDcEV5ib8H410TyZDhw)!+mqV&{0VTr|8DyCOX<1k71*ZKuT6@AbUk^s{M z>2W?YAXwq9^Ezt=NI7}VUK|_Ykfj*PS@^Fa%w?vaMI_m+Yi;VPcO%jtI+e-qrmC9u zl$%aBnj!g%j{FSmQt24dO2|=xh$N6Li`u7*ujt+NFkuJIA{xw9DVaawEQd1Ha@c%{ z6iqqhO=r<|7m_U#HjHP15I!CJGGKVgH#Ta-nhfa|3P;j#r(b_9+H%$BXOqU_1-syN zN6C#hN2>k(9fi}rC4xU=j-Iotey4!1ENze>tCA0M3*p>Yn?+t*$Kp_kHknXWL}s{P zM3mx_JLTuKa+2H$8>$Fot!}3fBAWMsvS9n<0Swj3sV8PrcI9>ui73R>XwM_5(Lz$? zP>RfqN{w{=b>4lU?EThp)y${_In;6PKm`br19L6WZd7K0FLI0gVA()#nD54a%-zE<+N+#UGD*WL*29WV3H+U>y<>YQ`?I~dzHJTvd`fr%VQ18^MbY(MkzSvO zv53yiR=)$`kXQl&2NR#VJzyK8U-MS|*wjHv|s4 z(<1k0DxTocF!Bs*zMEVUjq4l^D?kmWwZ=K%zC#Z(Nft9W~2oZlAn0BWbIqFv{} zfjl3Ewg5L*$fN7-Rw9ZK-?2x7fqIy zmPi~8D4G0ijv5q0sMpYl6UmVdH7~$_ZAb2?nxEg^HAWl%tUL`6-1mbPDoHg-*`Gd* zkEVt3RrdmYf#ah`j2Tb=rh7Tf_@fU{uVB;K?O}l)cugpg(@UHK4J=uIk|1Lr>$jLM` zOX1gOAT_Fd@(gCy>h455nLIxFx^wp-b8@n?Sozk$<4(w9I;w7D!FO?Q?=)ou=s?!j zu*HJrj1`9s?#kOFFf-cBN9n`a*Z?pQRC* z-V_yvN_@0AlN?2A@Ja6aBP0_Cg)m_QbKA;{LMbJuOG1m8{_h5NUVlA45);j*^)I86 z>3SHtiq41Od(2i?q+>gdt&kQxWYC=whPV4hf*IT+Bw;qVSG14{)w`HYPAB^<0S1g2 zE5IVy#xcOrz@uwSKq>IHp6um(_2l(-u$wkQwsY2;z00gc^ZhjZC(^g#>|lBFyzePr z8;iHc`~DEoKh)JUUDSl+YkY851l$Vcf9{O;t6zhppP`Qk{*W7?Yb?8X7vu4Thj^Hj zK-%0FnJNU~w*&b7gVC6r6@?VEgvE;xccV!43_FWqa3(7OvHbS9bI#c4m7;ADxO*sp ziLmpDBoH$lN2UcYEJo^5CI9(SVsQBARU0`Q47-qpN?hMj#g9?1l_M=oC@iVouM7F) z0I3--p_Pa`%Uxmk$m0kPbM+9OsX0Hh8Wm2-zM~Mw+yy615MA9GWzD(4a(N%dLslch z1*IayOrl|a@;xH64muD5f@?T1=9Xl;&d#R5Z$0}+)mu?x?8#}V{fuT zxqoW!Tffa5aFds6hvWsv`qg^7FkGsF5_m!h&vl_3kr>teAw~L%7f7wI`^$^PN9d{? zD;CS}r_@o)-2?*>>u#D6WHTv8aIeVIfshT6GXWkwpGcQ|7{-EC03iH2>mJq=(z}z> z>5J*P>@SL>Sp;Z8VdNaep7vWC&Z?qB+s*HpMa+&nb-&6nLp=TecCNYBe|4*&_U`?Xv zz5?E@JY1N4fN9HoA3n$XRv?34M?oK*W*qu&%ZEZTx01c=ELBMdshz;(3R<_Sv`4Zk zcSZ>02m)zEVDm9aFE{9L?lv&x{LM^SC`ueSM?ri{?!aK1ZJa-deJ`_j6;TF@F++5& z7os1`;r{|Vhc5Q1Ez#^NjPT~YFm#p{T2`Fc=hN;_USI50>f`P@FSA+bwJ;r|x_41{ zIN0GBkbVL3ry;gqpDb1r33HNk6_u=`G}c<^()nUuWLD9hoZ789K{mX3$DothfbW|( z+z#!QeE`T~~xbeOduQ3)o;n&~6U$R+TDT4L|0+3B^eW z_Fvl28%KxSrMzpd@RhYA6;abHz>6)hv;OU5)u9y_g+ZmKE~9W;1!y)_BA)dMY#mgFhx-NyH`~SIu|Q4*%JzM@b~| zU%mRCEOXyAmj^7FD6@8L>)gh41;v;T|N6#K`bWAqZ{>Q>F0dHXVw}{&Nx+YmdYu~Q znr!W7{$xNAutHffk~|`%1iE`l6K*RY_JJsk$3}MUgW>bt} zS|e`#e}Ej$Una0r#t|#VW*@iyiu&}fWA)KCJf`n`Sy2`X=Gg-K)z&OetAM$gO z&3amWkdp6hkDbXp9wnH%Q>pu{;t(8$Vsrt^#dvPh#F8XVX!QNJRJsB8ps8upH8VX?Y+X=yrMwGNGSHC&W0Z>Y+Pvn*(bJ)K=4vh)z2z+0FarbpDyl z#VbtIzP$A)G(8Na&ae)L#FZd<9SwD3$zC?2uib*}y0;eAJtO1kQtPUA`8=Csvd!s6 zD_q3M^b4q!*2umjLM~2AQUXwSZN4X0&W?en}6=GxPd~;guH9-v7 zwz}u`6U081<>~=HaEk-O%1L8g&_cVvo{SpipNJ2Trd@C8Q-L|MJp_y#pm5*T%9%^Q?8hSL1WpO^Bv*+cr{$K{@)>h4Ghv&SXT1U71>M*; zT2Ie_=A~{9F*i9M&^ z=}q^+1~61AQO!VG=~mZ2oE>1hi&{E9p__76p#k1JrWG#h`j!x1(Un;qc<+ZQw)13|NHaxUa3sJcQ&NEqNng8s>I^wl2)tcR&;UpoG0^SF5)ySvh(1s zz{Uv#A$e#QofjrBD4LcfR8X5U%u^Ksvc*PlU5aK3xo=D~cn7>%&Vk~#J1TV#G|WUB zGYBtnmoX5v*lEK6lY56#?SWkJ6o&>}_k+z=udyU@80?~akl<~cU1`G-T2_LX_JXVy z=Ryh&7D>t)geqp16$js0zDzF8E){S4QBV)l0WUkjHip1mC+Y>#*`ER4{2p)*sf+BT z(HozlX-Sbw9bgl2&L~E&~AI$Kohfe-bOV^aRwrW9_h@@5csryM})co2+`7 z5vFxiA|fmcuEbk{b?Ll`2=+2U~1 z(w+S)q8=pV9(g@k$R86#=f61bM6{MKl3O*XVx!dwbO`02=((28+=kJ#l$^HzXDc)r zMtwe{Y5%c;P}ZG6+cmoK2DS#77S4xtt>|qT9hC(Cj6)xua#{%sR9y=PaMpvSKqoK} z*{4iOX_oiXRU0$;JU4u}q>KC`Kp6Z)rC{RgV`+i zefRNu;2TSbUQ~XvwYG+HEz(Z8u9`Q;>1ot^1S-VU0}P4hViITeh6YQ64pZf|E1UPa z*5WXp)9L7yvnje(%wrN|jvl^tE=|3{{z%m%D=WtR#|r}>23dmU4;n?*m~q`!zEP=U zDbldl$sq(Z)qcRheBy21<0poZA)*jPfP^sn9{Xo^Io`ODfA zIQ51diDlLwyHF6$D9xbODU1YJ+LlsHtr{AIdw9h*hPT#nZ9FVQW;03UcN3=4_lHE} z`5fBh#GTv)i87h#*eBt(TfdBr$IWhAFsfPqvFm>)fc<;vpR#MF^DwGRh#x7~hzD=u zD-CTac5(sYZTulK%XKubK6`mP^vyrawE_F^w~M3`%TTE)a~?iT1PBOpNlEI|e>8Lx z+G_J!(|zv9J~p`#d^YJrH)mrScsMy`l&82wImoj!BP2d01GFixZ1}qDv={#Wt|q^e zCm5eNo2Qzf0ns zeb9BhlxnDQ2VfcmzSNPqZezD$yL!FDh1s~UFveYy#kz`~2OToO=9&NdEKw~ zlAJ&)EmjUIwQsP;9mui$$jh9Neb`fQ@~(x|Lhulx>EhlB*o(3tZhj~I&ifgZ#TbeU zTWRNZmpAA=H-Asx-nK+|xbq^ANnp60rRtC$zyB&l1M zMynB9@PI;*XoB$hDylCZ3G(hv+MK9)9I$zV?v~knPx$^*T@68DRKAh4ZZ#7I(k|{e z&Tj6j#nsy4JqcQ?Z_7?Et;;|WmBH^o7pyn69ES7ZQc465_ol-fuOapNTBF_yf)6|; z*L%uM&Azm!sw&C+^hKIT)EyhAdhPY{wW#!y6yHdMvtSIFwI*1t@IhCt{V@=`2x)5D zgB_81!!6oUdZ*V(rL8tXW+FNdqQLM^IE+x3p-#_KC*4HVhquASIoo~JM-|ANLRL%HrUW0}dB^3o&j#Dg2PIP`!h%~WA$#GFi-W)@n zO0IR2+t|a1pLMbL6Gu$78O{K0>p#ml(A14Oj=Klh5+KPO(EMH?=v4>*7l7VI0tmoG zF#34xRq_LDQ=5S@nB$9uPq0973CJ6~qQ=bLd;ncH#2=>cF<|S)W_3j_JcR?lyb!Ub z!53e(@xi7$qJrDWfx0d_f;W~*kZF+p;kAv>8gXRQ}Po~#sBc9x6b2T4sE2f{~-|DS`!MY_Yxn{}C zge)vXpRmqG5Lp5|r7T`@8PKVxgE7~-D1VZ!B4`A!Eru|yI5ai0Kp4ZBuKmaGkN-zg zO|raQyn-dFi)QM8SFN%b6m=ev9$qu(BJu*PsjTT@S{KK*j-cAcJrV}16p{UO$N0-Z zYR&g!Wz$)n8nwp&H$(ced2rLozhht^~3v4>|j+o07J|6e7R~d!+X`yDf-7( z^vaGM(Yyg5!qZX^fDR;{&`>k@ce&7UtCO4|ciJn{#IBPW6)$<``oJ^!I`%akp`IEw znE_BcFQ#}Z_lS)xw$oJyJ=-{>Aa&P1-vaG8VXMB~s?85rhYb0 zXKm80Y+&U0XZ4=fxF?9TO%Q)7zt9;&>}TOS023vreJymtX&D@al7Ja(;9!GsmaD_meZQw$KRd8+l@ z2&Jh@mav`b88vX<&faG%9Hq~Oc(tWJoA0`&`IExlds&AfhKXkwtS2?38$#68cEHV9 zZx7+$*CQj6uIJS=uTH2i#q)SdcMAkmz{|uLoQ=LCGl~1O#aN(2!4MS~G^pc|&L$f6 zuJhn{WzVLE6-dJi!FZ?0B z^MP{-=W`aneb3qSMlC+eq+tCx!scDcs0_1-v1Ewh1i}DWTjJEsN;w$szEqxG$c4 zPxV?!)~@iMCFjbyXE_VD7sptvLHyys5-IKL#D9ov4*65tbrtu~g;PQTG2y~k`TYt0 zkG+=j{-!ybwY@F-C$cw)uXzL`T)wAZx}omFd!Ml|a5T(kLGLN2WO>g96^jZ|E4Vhz zl|C%UBe30>nXc5{2$xc%K3=w#!CWE@N(8TJTnb>ymzRXM;Hw=UlE57(j)aQHFKXL{ zzp=5r*8g8Pcq3aV04Sfm_eJg;5?>v7SL3h0@0x_y5bw{lIKhjp<;(8L0q8uz}i$1+0SS zpDGq(%;3ELJB+Ec&dB zSL_61*s!8J_M$h3IQvD|D9Jx?Jhjh9XkW*8Lexey6+tlzuc#8D z4v$-NS3xgsSDxc@yRT&wUK*@RePUeFQ+_dj>D_HT`j8)vUieKO;*lszx|q!fQp|YT zF!hdE%8!$!;6vM3?=@03;}kdsuY4vSUB@!O4nNeV^#nE&)v=cn^RJ0Qvpcm?3m=EW z0)!%uYgLOiUlQxB=DX=Hm)EgDlV0j(XGdP9)k(V_bZ{PR4+ON(&uJ4DsPVgEzH2g3 zW0aC~AC0y@BIvC{_b%_XsbR0dzfTIC@uH)Hl9Mh`2X&N0EhYCy`Fmz;N!4GdgR5@> z@**T42Bq%E!3~sI(%llj%{Sr^p)6^J8y*J-r^IQ70tYxQFDt;?-H=291Sgt7a;7y# zh2c@xi;H@6lB<630PSX1HSBhq6k+@!+2zEHY^ND>_D?1+jf$@U3b;1S!e=@jeqZjp zWO5dDQ$g6PF&@D!$H2EmIc(rsr1VY+I?gS`hu$^y9bX@iQWL6#$7%Gr#C zjS&7*E`f_MO;)4N8h7xe2Bds0OIk}K^`#PC(fk_UQeBsHv5PxiSwgXV$Z60WRXFlzqX!#-2 z*v+NCP)V6a##vjEY0ZOmh@MX4L25i2q##pIepA~i8I7=sO{&5?z*k?bcKbV6|Mog@ zbPu!{l69?fz3I4!R!f@if`9X}^Q+)MF%Kv@roArGAX?0Cb&RZBqJt;eH;nvi zP)i|!a@+Fp`LWPd0>WGDYsA7@2GMl0Axgy=P*0}=cl4wSmxWI<>$J3dRz#~8Z^Hm* z0fAgTM!E3)9-MY`9Tz3i=HkCI{0`0M^~<+0JfH%mc9tHh$DguOFOW;wqsEQ?oRGMw zP*zNze5ZY87bwgD9OeI((}|0X&`5a6@6ApZ@DZV?ZfH3`#w`~OZ2R|Dsm)@&ho8K5 z0s3Yyak$Ig1b9RefMn-&Pc*Q{X*O`NhqN5C9dPI~A-k7lIJNw^v%(n$llDI)*BX8O zfu*6}4R=Q!!?YJ9sH6y>mAkXDs|xt` zVjH>;(ph*bY7HoOc*#o9+@qw=-38-~-DEzzc6nIb(ShVWcw!K}equ~f_1B9F$4`CL z@f|U!Ey^O6$6p|6lGLv}_jQN*n4|_5`Q(k95bs`d>UwG?&UG)?N_~UCO59yP>`3KX@aJ-ibDiyh z{ao;JWeDgHO9dfdQ|yi-UOx3u#;LqkrBu{iT{64;gKQ%K5e2#W;UTV?e+3@e{59I* zo6DDiV1X=q>FIZg$$U=YB@zOu;jjG+BYYP=!(eRyzfPHNl=5=`VNbx7BO!O5VKa_U zZd4L=>;MHReZ{HnYe6+umX0CVz``cn*Zs@&4z|u`!!Zm5O6GrfVxK=d;q-kO33%?_ zC?q!b{#FId73xM=teYVyQ@A}AwX~Hy5O--7WWzNUIIL66j){UXLa-^etRA&O7%0x#=U(-3>@{9{Q7_Up6?*{5F`SV3 z%gSg1Ej`&{pvG9%6BBr^^J>q^7W{_XmcKhdyL>=0cIw{t$7`|LqKUaBGSX-0f4@;n z`-axgpk~I?v7^?VoH>7$ML5u#a7W_1=o+%xA5P7fYiyF*8zC|GLRuzA!viUV+u)IA zqw;~{B#8ykBiV4Vc|FCfd@ztGmbDy2&5#~Pt<;l#z^N)l=Kp@f#HMN4L<4le5A{q1&uI7)XO0lOTc?wcKi!f zY0fEvGT7256&Z^{4t0cX@0))@xA=O5l-w1iyZ&PTB9(Om0^}~cylU-6F8oj?m02HO zJR;_`xP=4WJw6oEYaI8uMe;BUFfbG0$C`}?XgI2$kda3on+uzQ0Z7w9Fk}P(YG~i- zV3#Av@t$gNf*h9BsQu%tseJ^9YY5*O!W`9#1wofg7>w4iv9SvIq_ScJ-DL&2v{L8| zQ8L&8yAEGw5vWgsx6^=onU7i>M@~gurjcr~C7Tpiv&0Y|$NpnFQE(X1=oKR5pHvLK zCM}S^z9ywuGZz^upJ|)qdV95x=NEBYi81e4ED*)1#i1;sbPo1af0wC6g zrX+A%OA0D!>ovNox6hmX24}J_enVcS07^C6m%p2WhAf#Dkz%>lgKfse_uSONXc)-% zH!dm`j8AYkE&20nqwA|L-mA^obdheJQ8y%=!qtP<}WudeYtP5WKp^ZX} z*jYI60+xF(%CW!>B$M!G^OOcGK<7-Ce;CUaRJcrI^O4Kv_wXgS<+Ka>Khs1!q1I!X z1I-jr=Je)1_`NYUs5Qv)kh(vu5@9p1*B54O4KxeEa zU+MPKC1b)nI)uT`UrYcIMYH}KfiZj{0pyjQ_7Fpz0f5Jq0Jkz1M~I0tIy#?Vlic1! z0nO?hnNCYBC7rr_AYTI%EoEPIKeQ0?-290X1UJvlve&c6h0KL{5wEcR& zLB1|XBM;OV#&3)=&D`c&>;UHZ=%DfM0s)+IQ z=Ax{A;=vf55qO4-Knb9>Z>nvntQta3iE6t7zAAT6Y#2+R)Xtfh@uklWW-LQJl1_mc2*hhiFf=aB(BG=n`ySP8$qS`t!!0q zx%=PW)`h8j?7BqAcWtH&C}T=LQ~ST~fjfWgI`rN{X9CsAeP-{TGCf`Md=XyWmbLF1 z$xE->x^3u^#w$}8Oae<+F%1&mA^!az&II57aKVZ5ec4&&e@Ti`P2tfB8x8nr4@-)w zdtFg+yg15aA4aY4H2qc(gy&QT>|*?76?*pC7Z9NZ^{_h^gP*Z_EZv*pmWHDx=~;q2 z)Bu@k8nsxhL|2|Idv~LzY|Nin3J>cvvCz3^ou5|KYkmmq&}^R(+xjWSRxAK@J^%V( z^F&;IFvr(E3>`q_fqK;Rx-j*FoAdb_#i2v){HP40SSgHXGa2GiIf^Y`GC;5Rr)VaI ziPw1>ag3fn=JC$BNiP=4+AV+sMWT9b#oMMqZ!#vXP_qWO$B!i6gS3(`F+QI$<>NUc zZL(d<2zgOv>9Qb|krfESOY#_Lr0t&(;Ma zrI8_{Z<11~D0t{({@|kV0U5O>&`iIeD#dEE6iM57kdUMOrLj(73`}n9Mn_xAaThvu zYQs$nSYBM1d|5TW^O3CKJJ9FYEvj+3J+Y~4i7A|0)wX%UlP#YiKWl)`YhYtGJWzDY zr7$q|PVk~xap8Pn^BfL@@8IHywKOkd561egP|>_L^%jIyn}HB}I$~Zr@lNP3o}1ok zs9iFG2g{7Iv%~z_!0Zs(Cj{7$_LbvS3&qT+r;la9NJ6OC{zy5@t*F+ zx8va)IzgXcci!b-jMc;)hrA2IWoKA7-gjbbuq6_;*QO||GF*W5xsQP#j>uBYHCDBJ z4PK)zgH}v0U&4>~W6c*NyGZG=bq7>1iC~9n&k_zyk!shY-fE!u&y5D~R*5-`$1q>Y zm`FG8w95yGwbbGh+OqQ&Tvh%|@%N>l*<~tmF!}U1tZxji4>n$N{3p3qu8r6;d0W#L z6Z6ob@dTT_IO>RCl%5*$itgIqqR0eB>kVL3C=dPpuFUvO6g-;@TH`!4?NyyBhogvI1+U6Gi~a7iDf#uf=5R=i+ldQywu@ zgtU%(nZ`HeHDrSVVC~1z#2Y>|-P+#9luw2rXfvSf&#nr|xEIUG%5jQ9G695#$iqir zm!Y^Bj}`vX&-nASE4u)aV`N-zngJtv;u$Kn|}chcC%I;rOeo83g(JT zlq@fxY8vR6+5FK#P5t-?M44@5bQ}Hh0K`_|M^gb9gF1tO9Tp3+(!Xf-nY5=~o0m+I z5TAJohefX98H2n{6T@Zk(*qxCG+1e%!C;C^1v zMD6wUg9}+w|HHShE@9mSbGC9P=mV323n9ji;(WM>5qZ%W1r1nmj?4%6Q%3!$pmaIMoZST3CvNc#t6G7PQ07%-qk3im7&3 zQ!uqhCx9XK9Scw9qMT?~)k094IG2b+c8h_ii?B8MY>~v6p1Bopidfas+uVDc+sSJi zl3Nw+0(H}8a%?BGtueKi<|UiF4oUWdaylb)#XxI|Hu9R$&=9kx)&-#7Ar}s#n*9i8 z0)NjureLBFbqEBh6Rl`jSlac3rwA%B&KKE4KYOx&GnYYRJt3!;RlCnB;$6+jvc&h# z8>?~oY(g%1%3DW3NcnYdW;0w&+w(YMrK-j@ZrLXyB6O1^cr&7jrUI7m9|}&=7Bt(@1&CA zAY=IB&v1+rl~(xLpLd`w1`__~hM<(JGtP{%!6A=KTF{gUDwYr_@tNXY#whjvCNC3Q z^kY+Qw?_J|Z0kn2BD1sabdiF@7T7MY$@CwG0DBu}N^+dB;Rt5Y2`cukN({|)9|F)O zJSISf_c$Xka+hK$Af(h4!VnP^(!OK1U$C-Z)v+flcXJ>cQt6JLP7#`9)6y=qEM)Ce z&P}^W6TjfZXQ3v{ZM&w-a_@FJJC?dweg!kcJcVZvG7exhG8n27ISc-kRk9I>~-LSt67%^O}MY@1(cV@p9NpCN}li|ASGi)|~Tt(QNci%I% zgR{R27;?@S&g|3_VGmO1FiOSn2j_xxu=}$;zDc@At2$xx4A*@!;I=9WpU8mjN!6ud zM;ehdG{y~YLK9pgJcD);91<}v+9XKIV*&(3%Mgz^XT*&wX^pz9W*f;yz)_Tm0Wxht z45JFesW-qaCF*NU`hL?SX9Nhjy4h=@>Te6T*Axk6yJfmW!;>Ci2vJZeXW$!JhU)EX z6>MZQpryYia7Y}<0v#Se#|=jwedrd@{NGN2%BHNE?gbDAgRJY-V750DogjMX9{rr8 zGf@up{-0i`>Yz2D5bD_fkPVlL#rldAOH5N`LOc;fYp8$KDB2dJ)Xd1+UGhx(hNus5 zUsdS&pdi`0TVj;4jMMlAqJjQ3$X^q!-LB2hr)s|jZRE~Tus7`7@KBK)0>PR>C%r1( zP|OXV$69Ndc5iD4PoG~}Ph?vat0>TZ%SC-g<_{Lp809~N@G-I4+fTZPn(w%sy_u@~ zIy!N+CEfZ;(xZtucU5aWl{iG96D9Hz(;5m37JgNm)9)^a^OCVx>UY?wHw-j|wKv&u zQS)c$una?wrF2Fix#q zwFZDi*z2#{3{W8%;IH&-=(bS`kLp7nuN(GnF^3cHAOjRrc|+ysOdqtOgE%qeWgtl`wn^1S^4BNPmj553uU*s^jY#&?gB3 z>&Z2d2~$2?(&k%c*FlOH(5ZxZvFMR3E&;lXFvF;RK6oMF>0$?>NSjhZt`+StonWWV5cOn)rX^8us%E43*;=*gg0i*0O0LTei z0(m|3L%<0xpCI=G<+ae4KoMQ$AO3xs+2T;}z%JdhHIxB1?k8C$oOBN@VrDAccv%_O z57Em@=2ysHItXJGU`7qN9M&_Mr*P3Irap|gUGTECl8iqMGkb!&PS)-bmoLm+NF|ZL zWZ>m6r!!$oB(SPD$o6?#+=G-WjXxo6}ymX&6aF}RLx!cCa31uFx4 zMr%kKiaH$!8=Y1rGA6nH)i@BQnwr4JM<@eOIa61m$Qved@7Iok0myV9S}UYp4_Qbn3EsB{JJtt1 zOKdZ( zPblWa6^s_b-R^cP+)%18y(V&;ctz=L-$VUD94}Mb-8yG2$8O<^c{!2`erUfGeKOm8 z^gKLU4#ArYhFiyreZSk;)nN{*bXFej0n3z4J<59t;tJYEbGcw#7KgBmg-JC3eDkS{ zx*w;* zRlQ|>ztpEUwxGgYySu_JnzmERS*!L4b8X_j{Q5}NP1KO2L^J6}GDgy~dFXX6ZRn5S z;&Kzf5H4nW8|G?45gmjkC@((VLcD!L3}_{7};j`BUt2 zrcuU!e-zlHEcMpD*4lQ+Th0xyz`l)_X76Q`;t^y7LUG>*m^!Fl*Pk$2nh=DnhCh+K zMIfWi?+P@v4j#=`?f3A%bd#<*M#>VkJ?&8KKJm-wl4|X0OW_Ao)23kXKY3cy?MwHk z{sM<)oR7ci@(lO#bzD?q?#1n?Zp9yP%H|#E5u;AAZ)V*okpEGA?YtD5b@PpzNrzu5 z->fJZI0C)Ai5$#=RJZ5WoA0!U{0KJ*5TX?IA~`C7U#l%S*W-y#&>`|ZTqLN>Ed&_cv6f&(Jj%E=sNNn-K_ zoN>_)E-kR>8HB$nv1q*{(?kKv4B#0ka*mlBbl^bWI?L9f9xA#2<5;JP`d zj(!BR`{&vcm)PYoO^Rc0Ek^AHH^8AFWZ)!!>=YdXuISJviurPtbmHD~;i`OnkfA5E z*wp%LAddYGxk7n9f(GJwI&&VAevE<_gKw{Wp&AknNF%@WmY|mkJ?1K%*bIRSXZDW!c%`deV<@{!C{k|2MwpC{rA=&ci-Awnpt^&}x0A|hVF!lFr)?7R+k<7T!@hxLWIX%v$z^ZRQlwWDMG@GhIh zfv+bRiI;T~p6G7KiZ9;^hcd~>I<+0HHnRku2zF8sEPiQHM31EL!Wv51^V>kBX8EZK55L=YBRa zDo1}L7w>fvu)O-N*QaL38rV-fAlwUkN#^n1DWeu32t*Jr`ccIyro7(@5_R*0o3|~f zpf_L%s8_DUGb^Ud^iof5mo`4XxQiv#I3sOM1lxv1`n{{ZXGv)iF8>&+z^H%op!hXG z0eS{@S4Np#g*C&XwQM_W+n)9b45O|8-snjEH*rELoH$;QXMPYFur!wD{W0k(8I*J` z_9HmROCKZ7`FHou%$huom*+Z`g5eVVa|k=3ReWZwi?iwtpK8cyfmLo$WgwBG5e2b2 z;Z4ye#=wf2SCm}Am`%l(SRyjHpS8Fe4R_cPS5_OXc$BdGPjX&0PT2@-667k?gIBa$ zsINqec6TTIriU1*8u#LWfM1Iypb}nn=M*MDDcYcG>VATxGN_$ODc(cwHr(Qs(*LzT z_-XTu^>qH*zOtQxShCpAQm#m{heVCGBqRI9PDJFFPC~{OV)S5+)TDB&cOVXmi-qAd z#Ny2-Q%$TQd&k!0NOM}ps;Brd2KM_LDpzK=+WTpSBC^TdX`@1wdqaT}n~m-quyZ39 z=|ThuZ2*8~1p|w_ycIVji>A<&s2Y$*b+EN*4|Y8Gv}>RmO31IKY^VY zM2OZ64a;{Apb7O6l$o$XowV0vYs>%SmB}q1!UZWQpw0-wl;Z2Z?RKLTnqpwt?5U~I z_8skXLO7Eo*AtC}Cxa()80n`s2d0e}gf;H)bv52=4)u9Edm$Ci#DiaF^LZETfz>17 z_(sZjRtD0f5Dp53v`LT$7KIbYcLcccUrCYbt8_6OJWrK*1Tp=Pd>u|B4_Q!TU;NJ8 z96N*_4K=>N=6GJ&NzTxmJ)y9<>Y}|SiD@zSLk4q+(|AqOk~blQDVD`ZDmKQY-jE15 z(Re0FzQZGPZJ6HT*0LyRY8HOcN!P=M8z!^;EBOEShbIEziemm`y#j{m>~=d+V;=5q z)MY4Xs=PRz<5sMQ1xEH7o@vv4q{XrV$^)D4`qj0KZdlM3gs}|OmfXxpHmhL{+J=!2 zgt%r(p;|3BHF11}|Mq*V`bBLq;WTYxv1BTi_W;-sgtQ3~Ne$AS(mtgV0oDj310!QP zqC4Ttl>Sa)Gx3m>#i=Mhur=H)ob~s;@a<(nK(?_T`e0Gr_%QkFUePJ9+Y0Lfc-SqZ zsm@>-<3AhAO6ox6hC=G}R{wDVr&Eno@n0;0*}9z0Y5qiPlLB!Eniq1Qt0_$BxH|yBxvJxu{3A zq;g@?syx*zEv1}CDQpNiA#vrN^^qh6?>a)HR0DGiLm5ZCG^9gvXLz+?6O4`SFS?Z* ze@+mIH~mySF$u6!t6v^t^ z1I;e)=LKQ21KtxQTU8jbo-NVW($foPSgBC3DxTq=fm^WH(>h;m?jasKn8?-R)0fRn zNT3M>K{DprgyuQw&R5^i*1j~06^}Lqn{LE#+#Fv^%{q0VD$r)6@Pr081aVZrY;U{1 zUhgmeH@b;%tmFdMf|M9iWRBgfeNOxiz>${C*Vq{w?1;=I2weGB&PlQ|$%a+ByLj=Y zQDbBp1vK49^G?y}()ed*fCxn<=7*~pak+qH)`6d=Vp>4R>5*%Hz@l+g&3T=Cx?W3RrC|2 z!IeO21lFNeGP65)iLhjZY|obiCPV(l9_DM0Pp?(0hDJ`0s0Hb1g(l-#Q9lK9OZ2g= z3=k%~S{Le2M_kw)A|of;GaqDoH3{zgVmTc)tp*`IciV`o*#5_YNzLe4gFkHfPXO3b z_9U>>?{KEY8p2#lf4QaUJgIRsa0|TZRL(~?LJO&7Ck1_k?c$j<(z{sW_V;nY{cgDr z8^Sm3&_MrcbF$pUfbJ`_a>$d=Ut4!d`Qf}~9O6}@I@CfXvhDMi-ZkP48}n7LuVbq6 zh~!5OD5Ko&up$eP)axD7Is=nS)5Gp*P_4jjio0;1{-}*}!aMIPWWUG5%8pY&tn*M$+>%vSCHiODq0z}+4lj;Z~ z=>b)?@#fhDLT1mQH?6G^*|51~LSkh-&WE$B+sP%4<||;((kZ_9H&F436aTC<0pjC+ ze3A&Ckh5cdv~3z9NE5qJ*s@O2UM@x#LCI(qyO@=}B!IO*Lx@&M4wVrs(B0~X2fb#; zTmhcRSjTEIVay(fC5AN?n*R_+_qmTVA2pgxeRfG5k;^YGIoaJaZGQCW4?Gy-5kl<} zs8zSncVMiphaJp5=@(uF*7DCHC71A&xjT8D-)9^9Mb2HyY@zODJ9pg7S>~khp<(hneHYA`|MAKoj z=bFTr6SUb396&s6)tT~D3pcASL+yIMBaO+$Xlm_bdjq6CsS3+ztVGglr0*i>hk)B87G8 zd%4UX5KrI)0}+B9@4~@i8!{ZKUYOG&0^Hg2@olqChaf^1t~A0tdO~p|A*1o7@J5oE zL48RCiuqWdOqqm#L9#9SuI>w_|2uX#$p-usbayq1}n13}8tfU$Rwp*EoIn_EBc> zXHOlc^7PO$5~FaPftkbw@lC9!s~c$gbz+Sa6qji2Jmg2Ps}S7uI1RAk%wtEV)d7p> zzbX`^oXl1Y6MM#^Mn_$y^lBHJ@F_VDQqKW^$x;Z@i<;56n`X&?E~6XBcxS~K$+ZA0 zS|%>E&QdwxxF$(IWxQy%+^r7{5>f*i@G%;lTgYB<$FWZ#C0XM%?5U{1izyA))0LR` zo@b@EdQgx;kqqG>3tJNc-d{kaTpsJJL;*;LPU1so!;euSG6L|f?tCMEolQ#km%Ao+ z)t;DNaM&lxy76(xP><`odic1nig9KVLdiwdWQyFfu}Yw*c-}t>_}Gl%UU+R@y&rCq_WPOIH{wfNXmiG|KE1L$!GN4z@f)*GbhaphZ=jEGE&Ln zLOb4ducumQ3B z_6kvP*^EM45%vT?dSw+*jm4weKCrJWYPoy7R1JC{wX@_R^xyfA5el+7n=r%*X%z8n z<3@($F#5Va_Yl>RYC*lWR;=dRk(Up*4_vE*e>cJMUIOq+HOk1rNG^j4aTwRUK{g5D zC(L)AiLYJ${hu*E#5pM>VukA3GnV6`foD*R-9^bW?S|dF*5e9WD~o_aqfc0t<*!-j zm-_Z;5|#0&{*f=v2>mbws5<)OsW%`yms5cI+$c;vDK){tY#%CroUK3X>-ub|p z*d0#XCW@3IrK(2UCIwISiAt!fZ_C{@F9 z!?1NQ*zU+wVpYgnkUSTYk2)_`O=T_m10ZgmN0d%VCSsPiQAQL1CfoBtaho-TCYroN zS#KO!TdyLwPVR3*0$TNppNeIErejBeZV~#N8ZGN_VAYD`k#Y_P3i3dYk0rPly(4P` zXw;us4miB#uZ-BV8#tlydZJS zGNjV^qNZWBe3m%Llpv>zRcuQ2+H1grYm0ZH%D#yM(nkYJsVV{z=P{$DcvQbiguhz1 zds_FpN%)`1nss-Pt)8^t-S%>l3rsZRUaCy3S7c(G zJ=MF&#vOYN2@8_?qZU>EQcE<{c?cJP228zjn+cp;&NqPk8k`h|2Cj8+ zvm4D;y#4?`F76B`Jo-P8Zc$po$K9neaZB&iQ+K`5bZtFlgJ8PN-ik2*(*4sJ|g0_ zg>-r%_>1sCzMJhh>rvzJ2ph*!5agx3a*40Njt1UNNca~t;zI4Cc3rH!iXRiV)VZ0X z&^}xxa!+<6Ih2pe0@;Z4YImryR7yqg{CL_3?r0jKfn$q57KjTTyo&`RnIutx2^Ts$ z$UA$`!0J|9BhqjDr%1h9$ssFQ!CDZi(z<@MSA|*taSQL`T}{W~v!-GO0T?)vH;6RL zz!EL}7CSRj4uC+)nc#H2VTnifJSJUHyfDj_w*LrgIk;oc0^q#~FdSLFo7>ymQlKRn z{om<`?43IrQ^GZGBmR01RHme&fyf{xW(&KrozhHyl#Z9dp&5JUduR+<`DN&WVu^Qb zBE5~XXNI;Lg3}l@t9bx#CphBjUER}FsUNkQ z;F&s`5oZ{`zZdXH2J`v`sGsczRMb?2p}QUR0zQZiO97*le{+TbNHNpu%*)c3pig3f zwN!9Gqwl`R+cmLPsK)vqJr^v7zA5V)ag zY`O8=Sh9I@%cEaq{<`8I%xjo_F0tmiynyCYPim-0*wibyz2^0BG1 zhq$U8vs;}i`HEs2)r#bDHXn_Pcow?hw)vx_(Sz9A{IU9HFO~R>31$-A)ZybSnU2CZ z_l1*lh?K!mmbJ3NU9XHW_xFpS=}f1$BR#mlq_=JQf)^EX(CAU#Ve%p!?Jr3-=W__} z9L5=*g*JzU|F*pIiScsUE=$0aqmuxlr)~ZrM~2FWz|FEKTR4%=8NC{fD5}L`57*;F zzP6j9&z7~>M;c9Ke~55oP0kId2%|l(;V$bGMgv?2oW1fDQoeQU>jsw8QGe;&Yz;%Fag*Z@eoUvjd-F*-7c! z);p}H*}!f0X%p&WfuDtmLkVm?_Isl~RFu#$7nlX;Gm9;lH%r4~?Ap|P|E*DdI9%;F zg8H|M!(y9(v6~-8w5Q6gx*y!)_XLi<&RAOg(PfLm#C>y-#~@W$MW~nBad-=wWx}8I zs-GR+qgahAE83D-?#o#&)UbauW9(9h^ohzF;v@-BW?9YW z0DQClW|T2ZGLkWrag!}@z_nX@bPoF7`c}g0il;vxhjmJv&J>{CNAOhW>4Pg;&~ts8 z{$gv7jJBLn6c}5~ElY5&4HH!?okx6g0R}v5^9ddt6VH5lGUs-D{LnQz;zzZa)&ZP9 zv^u>^Q`9s!XyMx&*e8unyX}QdSOYa!PE)ph(?zXsoS>(d$CDfbtuvHB(z(5 zS0^JZiun)ARj7*}(5SS0;rdvvk3c(Ms0B%jkaKT|%h=~a+~0dHE^**%^Zf=z#ektX z3_)lf^z{AG&2n#u6|nr}!;Ppu*b|=1?+@1tCo{Fze<~6EM}R!JMjrM5D4{ zOLUTWpv|0g0GqMT89ebs@WK%-CEyohov5e!&tR4;=-RKR{EqyF<e7$k44SSYo_fd~t-ex)KF{BO-_ z@lI5AKlQQpr0VMx*20nsifINVy&F3jEJ0?{mxc!+_V|zqOevPGK zBOyezb&2M1e3bz8w3-CD)8w&kxo8py++5B|?;rXad#jOD>jidDT83X#tmD@msMhL9 zIjatkDDhxi9aJ#)uy)bcrMc(fw02DCi}lk;N#ZuZ#;TE2>}S41lJ=mS2odbCN1=TV zi4zqnmUbNG=LmSgofZI%sK*>%fE;1hHzM@=bj`XQ>q*$)sJ9A(FL(^CVqc%*vE9CS{V2{?VF#?XtnvU9ydy%HfP5H z!Q!>GBTlk-MNwF?!F}Ym|8x+@r1C`o0i69R3}3C!pW+(9S94xLMYR&IS1P2uBa@YF zAXhE2TbCFwvj?i{L0ggx^NhI7*6Vzi#nq=FqrYsEh(XkUFNp~YwNiA@A%G&~MVDO; zzSlhhsn#Hh9d~)u3hx zAU?9c%L_6#jbsnYR}h=*3d_L1!w?9oi)webYpadBQwql`C0IVV4E+BCh?s)!vck%B zi^vYlGE!M?yyPi>V^2}V8G1x=5NS|0UKy{ihjqD$8-i=n2Ky9Ky=hKV%;(DG;&ZO( zOBKH|c)cRxmgci_YvtzQvGD zUvrgF4BZy1u7;mBMF;tI|ykH%5$dad!`8HlBIw{9jU z`~y=jukd1DYR9*GP-7BOKnl&fbO)U|$cM}-1k@@9mSusNwE#*?L|#frg+?wt4!l?_ zh~5df{o{dt8Pk{`heYC9m+0upZ#RU)>aCsGl*{kEQfvL#Yc*vW*tm5U#RB+=uMKr= z>DA}JZo)2763am#0CDlI?8C*8hrAcqOa=VXIpMLa=q|oQzco+rZq^n!NOu&QB$I_a z;p@G=M#+~=D@LQ7!PHa*+EY>cDtiaNNzuAXv=bK4nK{HxNi{Xsq! z+JFHn3$`YXikX{2wup&givH#Qw;HuB~K$ zO|IJT`7#RmaWbgeaX9IdNEiHDUavrh>3H1PpST;a+z@%@qM@K%<}K_V&fV`k=ITFO zi8k3lZY&mI;h?;@RMLBqP%g`s?1>zg>xUobK1%ESp7m|o9T;!y}EfSwIq?0S)2Ob0Wq&YbQ_d|i0cz&R5~|2hqh-%F%1f| zAs1C}`rvbz_pyhgDnpD^Z(T=j8WhjU_ZGkdHG;R3HzYsJtqM^(r*3<)^m|Ej|HK}u z*fn@5Oi7Ej!8%4cn=d$R{CL7ERjn$CiE|?(eb=@z3wd?7>4DrKX0jYkU_?skHAdu0 zgV2fOF%7EDTX0P6D;EIoZ_<_OHhtv*9COg*-}{tO6awB>@z@2vQxhDma+@JhL2sCLd-#2x(x&4uX8cX|~!b7dB{5=io<%3{8K~!hZl0;$+h3A8dd5z3;@j9a>?MxhZN>Q5wP6Czcvsg?hc^Ls1n zw0;)y#6C((?H0=$0F>++Q1s9TqYZBx70MSwtrBf_FfD(yu`-@hDCkdwV{8WksG0*? z=qwac+=#-d#S7v+f$;uWD)IOO^_ZpOHYY$tqLSsK-q_>!3KYn==Uf{a{KV=piwA--JLW>?afmmJF8H@y(_S(A^}{ z#zSeo_1GM`J=aq)@n*6!t<`gLO0qqD_YVT-a+zYm?xRQtYO)D(V-=1$DQE4QX7f3< z)3g+p&#W?N2;UwIk^s|G$E$+72jFd;V)C-j#X=lxitu=EDAC%#wpb__(20z;5H`wB z81Q>24FrdV`+!&woc|>9e+fUszydOp87z_Z_8-9flF0g(q}*yNDffF!GrMF0%_?Z}lbj3GeC%$Kl<=uO`yAbzDdQ9j+s?tP*|Kf^A2%VX7XCC@;F3YM6*VW4jJ}rw7dAH{|9LTU zw7V%xt(gu>b2E;FgHib%R7~UA-%$LHTVY?s%bS>>8RmQc7hrm?nvZraf`|PE!w@hU zV?{3A$V}D+#SB${<}55aIXQly@hP?3p}_m!n#!Igity+=i{Dt|Y0WXpeDOVu$HdLo zF~Ms?0ev$2`ayHUWFgk!TKW-!H!ktj1F*&2uwa%6R=w;}+4S@!e z@Mb{OKn4YM&C|qCJC_8avd4pnhKPDGUU!9XzsPpEAOR=3%WAN(ksDTIYqZ=I!=dXE zPJ@Rz#aS8=L?~}Sg1+&m+8QyhYtnSyy=bo!WE4uPt3a{nqxX70v%*Q^rY(Ta8Ft8s z1cnm36)fsonK;terLjdy=DxEj-}msGAu7|m8Eb0-SmJlj<%E_dr;|B&=zk`b>K+t= z7~HM1(%?o$QaWqQ<2j=(^n}GD%!{DA63#H)H6nSf>No0ARm1VcL|Hj zeBN&Wd-=LG$IG+x|&cn(Jh>)J%DJ$+F@+ zoPj2YSGAzG<+OS;SKET+Hpv)-!$vGKa!Q(eUkANDp5L26N9ZdczO;S+kdl{AmOJXU zrf7sHS1QSHE2ef_N_l&n^0x?&T!2kBXJk|WuR z0h>w){Vn$j1bkh8xKFG}jl|F!nx{I#I@|HEBX&DfZq~g>xJwJ|f|mToI~jiR$*p|r zY}`KU5CE5CR_@s9x{S_epNdY+#;A@^6adt@mE8vA6#-GHrtbP;hnj+)1? zo#nX{yLju629p8>hkRxfTMk3F96^sY#x7EJI-_fEy4fV3G`e_E;&l`GSg3alGMH3P z^aJs>DmmFaw`Ot~0e!VbAND^mTd%1)#H{*;2;JdWLE{ZHj_3=g##r!px}Z8>V=M?& zN_>K$%5rd5Yxvel?A3yQWXuM=D&cPzsM;bK`@N)`lsCHVLY%Vq?k)1cOB zxb1U*asxN#5T)I5x-m(cSpBcwheCOObde2C@ZDa;Ht4FAJBjI+h-TbU*+N6)BV6pn zLg5`BjHgQ6N+U#^`BE;XpE}PwZ)99PpN7I}RQK=&p&3H{_h2Rg7nLO!O~7!Z7eDCQ z9`+tZNc(ZWI~ndKGY^KUM!C?F3s}tmKnk*q*x<;H2a2{v3y#L5m(4t2Iw)=WXeG22 z`6{&}m0d6`vZuVIZ47gi9>yA8KRk<82|L)yMP)A^??~%${J1!ub9pYBy`y_F;83nh z)V)4fZ$VDp9Mum) z6!7iNb`-cpIyDa3O{z_+w%pgrfkHbEwmq7jz_01%2eog{ZcXM646Vt|1>bZqOK>BT zWq)9YaxP+RXEsjk{Vm8omkDAu~@XrNyM46?oz%+q80TmI~RRix!c?r(im^n-6jmOb7Lt50j?};gJ$~AgA zY5`xrp#zJDnJ|F}PICc3-g5)e(`EN(&e$yVbMox;FvN|d%| zpZM*s_oi?A9AUwV`<->OSy-Cw@G$Jz z7Zsty?m!k@Ryt_v;v)yzh%S=3jt`&~XY}7*BdDtDu!iunk$<8}c3c~=Zx*NOb@``| zy^}|KBxx9cm~REUCG36R&R7>OgF!tRK+rQNVHAa(+nhuk^d$f6IJE2nKeGzRoP zQnQ64!uO~X26VbY;R=!{{&ZbQ_GCQ45s~cJJ$bGyWf>ZLJ(wPm-JGTwx*NTgnvu=H z^vo-y$E$?`dK4iAeLIS;z4rhEoxL|Q7vh>jh*BO!aMK1dLv@Jj%g{XWle+269Ha|~ zUQe^w66w=`2#6%%DWo3aq`le++UlWG@funcIw#@tQ>hE>bnR>-r$fJL5)6=P*-UCo{eb~5eu z50gQ;W~*R+e;!`W;R$%J)(LGcptm zZdszXUP~->KmwFfJN!G60d%88f1sc6XD39-IIG5(8o8$cc|QLu-Mh0brm&yyRDdQ* zUja1KFOB&ZatzGyaH2}hp--qceo{i)=?=n%veqmWH<6OOS`3(qb}0CbUsCD})gO0- zte$}CKNZINly%8)Nls?yR2d|UBE!3TsI+g8a=x4qUE{R-& z>jic+rlA8<7V8N9L^K2Iq}zR}kSq~o2P(mo>1wLD5>jdl@b}B9m+>Vqz?e;gs?8<1>_SPo#HXnbYnqn$z^jm#sl<7C;;| zah6j~wlJix_oP}q(K?eaA?OzCckY%$SezT(7sGDd~%--;%9 zj(sXm5sG^_TrRKsO<20F{9HbXy!DX@CUgo&36S)semJ;83X4aMtuV4l^)6^rE&!$F zfGRI6z{ne0Y%b#F-7I<*l!j}-3bD@P>}hw~WPa*S!HzUb2Qge1d?9YVV3PSiW&gkK zGNPGvLW@1<2J>=XpgHGFjO`(I!DPOa%v={W*{@I>ELqP5H>tYSP1$Tt5;>TL2EyP( z{EE3B5+U6gZlN9=sG^kk)*?{~fYX@etcib@S!1#EOh)Q6gn{7i(jpq!3=?{>#4v|b zXDpdpLf4yOU9ar<{GwX+O(0qFle3J!3l2E`iECw{quEW!7^J0>F!=JXQ7%ArNK>{g56lhe6|3YAvg;x1neEu{ih!46LTu&5?DhRuFc?1Pv zRV7(uv~VR^7G3=*Ob*$AqH6W2L`6YEQTWp*CG4Dzli%S88^)CB<=m`(j$ZC25;C}G z_fHR!c&@wZ*qnxP-IfS7>eXYW_&I>2tEhfOVqDSMJCN^7-3SgV+@y7fTJPb9-3Z>L zCBYYaffn|%U0w{jy$&4~UlfS>t*CuaCxiyG)dP1D{V$}|Ty$>_1j(r=KWx*ZraJLK zX4&$3=j&G7{Iuxto&Lg|15y>O)06^%F^5>7|HwJ??IwgZ%Ez3=d1*a6(}S5K8$cGjX*JL zp#$R8Cm!h*-Dja{U;h!d;p2R#F+qY20azbf;(w3aIX3bPhx36$XU~##;;95S91+uf zfd|NNb=z=gFD51^Cu}$Pbd40#_=plSfvvsP+q2E=dyP$ARLDN+{5zP-ja_J)IUYNsfc_{d5D;z}3 z+BpAz-Sfr|wcYNeD;}JcI?I(F5%BB!cwbde=AfnRCT6QxYZP9`I_h=NvOf2lLxC^Bu za_llIH^6EsA8%{=X;@Uk$E@y^{R@i(3oz2i=&&vtVm5>!yAY9xQECrrFIzth&U+cL zW;svWNNRpHHNTHr98X&`z#`mIfhMkMoU76JiDC>|byYc&m;fJNU}+!jV*?K*b2H!a z8}^*}Xlb+QF5H=*`R|ug`G~vkKp!r|bF;+LbI7D>SCT74SA{`#bv>PdA7`d+AVY?E6C03Y?!5Y@oqdrZ^PEy2rH`c%#sx70QX`dhWmMVHx;Rf|@$Cj7-N38h!(0m3!{4 zL(+?jg0xpMSY~tv`z}Q*{#5JBEf9+i@I-)ybe@b;`Dza&vkj%h9vEX5+sE&P=xg3} z>tP7a-{32j3utBfa~Uhu|5*N)QYzD1dberiWeT@I+eVL1_g!^jm4h501D5ebko=Zy zW$sI~Rh#q($mafCx0DE$;zk#wP9`4Q#lBN>>BuW5J3?`|%@o9P)iBot3MSRXtRTXW zd8n?SG(cU+lsycWwwsXh(Lh~SkbOIMb3{HcNb#AUpwJD7zl@2byCROfvZpL1;+nrO z%2y1-FR+7<{c9d<>C@I9liH;csVal(nlA=#RRd-Y15>hRl3$@OasemUByjW8DY=|B&7N)odUrt$usJ z$dMV`5>3Cv;$D&4-}ZMgzGs;tBtld2QHycH+mf2K>s^X(#2+(E4`pH!BggHk*)y_Z zA4xx~%cuh_6Z_Xk37M*XMO6H_o~M)>E2f zHVwI#l0B-JO8bYS*wN}Py{S3ni^p#3%4|1;2+X5y(t@RKdM_LWvR_`@mBfV*e!nbE z@4456zkht8D7hz3fqU#@QV&4*@%EY_z)!7(b;wJK6)I?Qaf880EY9X_=EmT}zDoG$Zb9gXwD z5SoaG+xk)GsBqNRy%uaFk+;yzT(ciELzFx;z0h|a3J+T-AbU)|hT}XU z_$5km9aL3ejj#o1YSe)ykYeh-&Pz|!`wY5Uo>E)+jhdDKnyBl}G7pVM_aL&4GObt6 zzOx$W?^JVpwIThWCaigKo7z}f4ULBs75$lB!7edh`HvoWiVN$qSEg1{{uAZi$IQT3 z`6yToebm|J3sQKNv9pLW{Xe^dF==#e`Ay_ghauT2G&bozLKM7(D;FBhh|Tgs+d+p8 zuxh%joi;W{y76S{F+&4XX#smh#ElKJ=%rkO6Ke+pL;k=Mx>mxo)9g1}$^Dkb5jF%y z4Nm9Zm?j{aVrZd_5vz{}nW*D#NA_uGu!$EoAGF#1uemY7I>iICXiBZPxBz7V5x(|o z)jG&im>V5Avlg31mgozUY11jKgYn-$H=dHKvY^s^)Fot|Qga^GsNdQ(z8|U~Fe`+w zrdr*oonN?td+LVH>bXw5+kIJzcANR4j2&`q`CWE^YjmnYu~=b$S+we+%PSTMpxQODkwRg8T~a! zjIDTgg#?MUSeHmqyZA-*cw8+j{VB!o2aa}ykg)D$j;CCxdX~*_XSFi&<{zQ^>zsa< z&j^!r!yss;y`!j=2aWc@YCM89qW+omApBrgt6r~e{a}BfCjk{VSX;l6wu<4odS=;u zJ0~k)ey7xn!s7jA+*HL;+fpEs&^SG`Q zwRzO`@B3nS^!)+H6OQO)32L=+PX@$#4K?}81Lv^@4YTf?`XPz0`p+i_5Gb(gO1Ai> zlrL))x25_Y?{c9j!T~mopi7e3J>^tQPdPk&E%(O@{UePHI|8s7Kep05^*F@N;fghN zsgy9&l<~==$CH)#^wxY*c>^;hRPBrCV7*4YAm#Cpg3YFWe|OEUSfG%2$)yV@u1>Hh z@X7G0uVz(`FXIzUeMB5Nfeg`Ro(x92Go8Gm?Ke!ue&>XBLrf8Z9#&nR<%@k0BjAWW zhn6rngr-N57?iN5ubcm_^FIwgGRl8YA-v`HY#33Pxb>T!Y%=#-_E)v*F|0}%n<~{7 zcxrK=`An=+V}qlcX6%dX!3Dh_AV($9`%Jo?M1ZhMq&p*FW=_E_hKT~m_wx?04H`u6 zKx;>gywUx$6LH98^|Jml-Lc<^*3!0or*<6!H(2nxq>lf4GE`+T*he5g-c5rqIdq*P za34nai!hw5pIANKYcqhbU-R;ck=!*caOY3W#OXn{ft)F;!sT^jR&eZk$ zH_(nX?1%}<^8r$SS_?#Uj;PyR)yd_XJ_eB;mdNCz#5xh=7Pgc)f5(R54OH#?zC#(r z$sCgBL8tjP(EMTJ3dS#ls+}pq{~d~~+^QmWkd7CQud=4cgZ!U~`cox$@Sjb2X2ZFJE*u00Z z4Jl8GH7lWUp;3Jijp}Dycnbo@y(d?BOky76+KURFvry{H$the@c$kYGjtyGb$8NbV0GE5yTT+4Pbe(^ zrNF;5d6u1;LD+7~E$)C#@VLc=N}FeSox* z=z_WPD97ndtE76@qlVTgIQP!Y?3@49ivkuVn$}=6*`3F8QH z{0%}%&Uqu-*A)WW!lk#eizYG|JN3|Q`)MV{N_`dTCKn4COeUO%=_y0lwP&QqE5qhh zWtE){uroMc0HkCvYu*%3cvUhwP$xGC7Qi^0Cg;QmgZ3idw+cXQ8O|=wln8mGGoPL; z_De2K4Na{7Ld)Z!O$+F+__GP}pN$Nnd=yq;V*aAi3quox_gN~a_oqIoD_E=6q@F=w zh7g|2yf0HTJe~r`i?uJUQ9AtL0{rh&Kz9H}Ts1AjJZF}-8A6d%lhQtekyuMk-mn-52xT1XujjLnV`NatXCP~_G` z^x5uO zQmGL6ooQyA&&F2>?%kD_@@FGcwI)5T$2&WF6&0sFIFr-zjE(J`NeDcr5kn{Zy+T;0 z%MS@)--{E)v@fCqChD&kKC~4z*|xIQR(W== zKw$Wy{Khlx$m3B!(%1WHgGSc!p_z>V%|d1_+OgwFRU8Fkdwcb^+ZaLDJ55szNd6ps z9jyJ4AGGG!+(2HviZqB zI08-obi02J#SU?m+GCXevgsnq!a` zmi8b~^mVZflq66dx2%@~wCSSXCvMaD6h#rWBidzo@8V22>HU?Y9jnT&q8=yIJC)U3 zu23kBBLxH3+_WBT=Pz?>wrY65<~fhLEFfBXynQ*mQ`Stz6?rt<>g(;(LJily~vfa zzHJ^U_y!Bp{HaL0UBsO98X%K!yRo29mJcq;4Y1fRW7amN4VPwT0o|4`lMDD{|Nn82 znh9JPa}cHNuQ2D#KAV!2^mTw(9%4`Kau6A!OFPA_#-dUnilZ?nBw7!Poi(2IJ2f7o zzzb^R58BI$pb>lXJ$x*p9{^pQ5omFimE?l7D6e?MIFYf2&^%%JWTe4Gm_Rbr5Fr*I zUS9)cD>jr9_=R4qnzb@D(#Y?1U_@_f02^B_p6@rbW4kdfI3clelVV5WN+4y6EW`+< zQ~ATB+7JmK88**M3c)%TgX2PT%f-${?_68q{60un*0D-+4^NOnyPJ5lR>hvdN@!m_ zA>JATuwN z8yttYtq*3e66Yxz;u8E=l10Dgr5^G%Y)U1+i=(4qxU*;&!6nUog#D^|7Ds?;162VO zjOei3AfTMXcZ*3*{@$alg8Pxx6Xok4A{Q?T>)(E2EEDf>6uxD=JC?BG$~(LSZL%2@UR}?dPN1psloU9UhZduyd9cQlyQ18=D6&|6_hZ zr*bY>V*&JpvzdIZ>+~vp?uWRhl~^a5XabK@yRTcR(KP__NUd#WiJBI`p!#2F?Ho<0 z)KypCLVJ;*H{D<_mo);%@BL#E%sO)C%@Xaj?%^+3Io8QOmAd6e8dI@TdnV*ZzG@73 zmnT_1h1-O%ki*aOP682{{+-K&bngfAaKw1G+&0vmm-pwx19vLh!*;Wz01Lgr0AWw{R3&(F z{EY}&?Q@1Z1j84}<(uz@|MPlhJIsJ8hzO(SDoO~x{Tm5O|E9Nxkz939Ooy<;_&|rT zWUsAc@vG*cnZQOS(a_NNZGrt+3?&_ShPF~tinkkLDq4MMNu9Uh`gowyBdV1Z^f$Ro z5G@I2=|T?FMBky9@j5D_joi)A{#{%V&QI_!eob=#f)*>0h8{{!rmS0Tep&bYV-#%* zx1`Uwz!Y=~E~3wmLl5$FKOaKPrNwO0<)IGDmvhx=_!Vp71iSn`WG7?%0v3fVQn_X_ z21i^n8cW9^xv^xr)H}3I&daMf!VshnUNZ`Y92ouzhgO8?azV@-5ajR^z{+wzU`Kpz zbiM0%_id^CTgA^Yz@M6NID-qTK_o&60ch4JSG)}MkiW?%#tTbYyg?F_XKY9AH6|C< z$J@&a^s$vT53MdP(7lo~ez7e07RmPq>88rI@ssD&L>7UW+S){j-l1uaa!0 zrqjw;PY>kmW6mR?|AZ5F9X508EJJ(3AyosHY~YYb=Gi}#GZFC38~HvqHHmtAcI_;R z_YLqeSSMTT-P_egw*e$0ymGqIdnx{`kA{hVw|0jRAsf;9lM%HN+I+e>?x}MgBxwK! zMlEsp*}qO6M4OpV%}n>u3PBtfUbpCn?rp2J*dq=|IISY}ZI}rG<6L4LjNmT^1kioS z(tq=c&Oo&4({riBRd10FS9O*L)6k&K5F!H6h}I2B%vHtrzkUe}LQbTYTdk6s`i|Hx zp6z=dyJG>oPy;49ej(fJ5kirLP?Pj+{#x)dB5d@4o>3KN=6*7d5!RZrCUM(JOsj7Q z-K?&_x+=p!u2$vsiNc7?Cm)igSuo$?VIPO}e9d_C@5Z&sR}@jzTXYwyWMKuB(*}9h zR(GaXZ80j~GccpHujJB{sQh9FuJR)yy`ZN0$&o*>n)ykzBiWp&%dTxG4=f|$kANFh zb|y%d1gE>Cq1ODdv({nO^NgT`e+|3>Q74QT!y{2rmM$7Qa=ov+Zy#=Le<9>}%FK#4 zh}W4P-zm?p8?vam&KJE2bRFzV)k=)V`KNAh&u)FU9X4ds&DgiUgr$qg32x*J1LV$T zSJth38gE#lRsA%-7%i^6T!B%KRj~aN4}_5`Oj)P#y0hl~GF_M^=L>&Pd#Q^^?>p(t zf51}%yQ5QrhwO&OL*L)XEag`HE=sl?LHjAM<7`LzYKJyT3NmtTuq;8nwvX<7$1_>A zs2x<@S;x}iz#YcNifyxduvPD6#PXH>F>kzIi|m&YPXKlaw&7ub57!m#?#CQ3A1MiiA(c=rDxg)@D1)hSNAFw6uhgonO9)o zsrVM_WXhS*r-cTI?IqZ%_-gcn6F(%xC3#n;d*VV_FeD`Vz!k(}je(G@%gpl3dAxG7D=qm)>~#{Vls?q{wIX;rapvK4-tq7vk0VYqxR$R?v;w#4i9_PLY7@*-6& zpCi=tp%>7CELzXr3iAap%^CEdj~g z%f2EuzKhB^!ryl43{C$RO-keseqcLo#|wxqLq;<$N_t=c!BuYp6A6~IIYUUVLN`sH zkx=ScwKU-__shH-Rw2B&TO9MEo0qS}!=P=7v$$q*rTeQ3T3Sb@l$z_)C+W+IaOW_? zAZ0Ly_G$rU>wf4P!Wim`op`8Di=J)sEwW&>{%{>^(lU0Mbw}zLXXifW9)ZG>l*<4J z-Lnt6({Db?u)7H*65s(`N4$uCMo~#(nYG(#N&I`-WX!8!I%sc7pE)Yunw&zp45wNV zkR0u1ED_CWadqHjS#=WXZNm(P7Vr|~_C2=4wP=HF1}~zbjycb8>{=?qElcGBNN+{y zJ7eC&GInqZxii3GN3$TP+s`Bg-qO}h|AusM9}&u`g110K~ZiI*y@ zbF4uh$(dUu=YQmJTJNIWk*N;ZzK}hD;F-c}CDcUDbG}|X|4NLb)X`~hg`@GUHWk_2 zjelxL`FLpMQp3~m9I z(dcJJ{(!YD5MSR|G8K^=69g#uM?`x$Mp%6JxZc_00FNDUfaJ_rm-S68gP?>AaWYY) z-Ys3%+nG+BZtgf68SWf|+e|?lrgy&;=CS+Q46pG6=`(E@Pu9u4b)>&kyWHQzD0Qn$TJOv-N@K_IsH4f`$yARYh*i7eQY5HsYdLr z-Js?DOuBR>LpNQd4*+haEMD+Qf|#C)Byq8646Qf083{y0o@`PW(yIgUw6XW+dF|a3 z^0Lp$3YFOJnkF#QNKdhPg7z!=A9Rd#eUHOjdU~E)>@@KVVH_7oD0<*Ypo7fJ-*|29 zxNN;aXX@rWMi#z83yF#ysNb1QBaB&IlM^kfRX9Y2i}$av0+n=l zoAf4gHk=<62DLqiO5`cZDOU37-2wj0^$!L?05?F$zeX&1;3gG#>%w3bLOqWRFs7ix zG*39!KTiPf0%+sdBb99Atc?BlG|M#cR8kucBWP_KwLV}uz$q&kgbH3Ge|2Gd`@Xse zn9$-%NsPI|)m`0)DBL#MAD%txmk7^PRHX4&e{><_7 zO*Q>BY0HbE>2>Xc1-YRtIDg+EzUmqPh)%>oloW>>5GUT11%}tV`LXCzlwTE%WnKT0 z;v5&<(~KyPKZ`+5i%kzEx;8Fu7VKjGpCU|c%(6@zg(?wRkK4A*D+LS*sV+ZbyT0t! z{jTQ6@M#m2`A>m5lu8t__XXKkdHIj>cy&TKf3M#XpVyXgn*K7TXk&T3yH3u_c>o7jrp6Iq34jwQT*wYL!wK z&KRTVKjI$fnb&O@)2j9irKKan0|m_lIj?uf!TwVnlYm`IC^)g4e?~rKo6BbS=PSfb zs1rb_)Vw3Y(7EZ zy^JLuD7T-of)#sY>$^itg;&Aj+3*o=3MV6V{B#Alc?YvO5$24$Dw(L!_Z834|M|Qh z0)$-ga}wEfcCb7kB>xMvqS7In)_yL`pe0Q)j5f*thl>x7mxmK6Xbo3gt;$`TK2FQ# zcL$OEMq=Marjn?|BTW7kS@3qBd#EpvLrFsIFVY9i2#aa=T#USv{Y_(~624&d+(ZF( z_t8CDiVuOY>^AWgF~_Cv|gDpW(1mOxk;U(iNi{t#Xges;DZl5)?GOC&C!wiWN+!C`Ef z5p}4+2TfbuHnmCU`pS1uzb>+tn@2K%k+!lY?K-EjcKp!c)zoV8Y>=-GottG&YC8T5 zWgr^@BO`{WwUJ03wq>P3K;xM0xlGpmMFX{PvqdrtEqwa54WoqKg&gb@Vk^UdYDgo0 z{$WnNRr2Jr<}yVv63dfZRZ&Qp1XN~T<6#c@vhuj9rhR~IecBBuN^k`?I_C(!<2v@hAN5NIpk>jONF*BX4!qof)khI{O@X7QA z4f6H{-d}{Uh8L!P>WDP%PjmQwXp%#$A-)d+h21d`Q-NEt=DHI8D~ZKgT-8 z@v`~NcrZudY*cTw*;C{lEKu!F;5NsDkPhj< z6V(C3@T>amLu>vE*ReNYc|t;1kw2<}9B8R)tY+@`Nqcfyvw^-(^}qo9y{mfWtKfjf zfY({?Abs@;U=v(~-%l#~yO@=!mFQwGhQW;t(;@YRj+1koDf3|aEW9NrgBPt#mu!eS zQ-lyG$4ydCj|V!EO7kMaXxmV`)zcg75<;v!s3WzVH!iduOQ;xS60S`qxugGV>}qyf zf^My3SeeFXbtVOmDToz5AY&oB(p8i%$hd!BAqvVGTPQLniU!vM(812ssP4{F)};r@!ElV6Mrg9;=pX(Zr7r zz+&XPf3ynNh9BAUcuBHw=vHs&Yzl)S7i3lfgButDTE@n>3AsHW5sz=Kh z*$ol&setkU-t}H)Im@%g?-;W!t}=&m4VKT>yP!J$Qbf+~K+bd&o|=P=#Pc=v5S1Sy!yxK#RM>><(} zBM}S6;xX~zkoO;)iKL5lFfE3?c&^2BI+|JtCVMU)9ye?cqz-~D(xGXb!R^0fnm!PB zFN=$!G)o%SvATf8wXl(zeV#x+dxD?U!f4K}n!>v$_^EqZqp6*4692s9bQrug2_={Q zD!QjDKqZnG1AH`QL7WY6Q44RZitsuC=>cE6M*W0X-#5R&{N~ut=nGm|lp-_`DuF(i ztf+d54`3x1axxbAPu{&>;=RYC))Z)JCx4$AKiS8z6GAR-_+-?$5n&GQ@R+*W5^*Ac z&!FDadg~YKSv+!$IO|_Mj1|1WKdTQdqKl2-$7jbKsj32H^r_vBpjBJ(^*HbP)D~(8O#h;ZT<4P%5H1Mfqhj{(W8hp zmzT}a_aWy zHmfu>amKWP?aMy%M6ildCYPJ{Vr`o|3)OY>lwR@VjXoaaL$0;j>3%vOYbZ#5 zgEC^dijzJ6GgiS>Vo?qkS`$31@Sn{uui?r#S~M^tR`AHGGK8$h)ysrSGepzUbB;CV zdOL)VnR242K$TA3zFDfud(y1F_HVAGq!Z##t&L7(VD(;1Y?NbuF+-J)pF`6bEyaIk zt2du)WQ^}2Kb4NhB(_YF&5BSxL3$p-pT`MO*)U5sZ?!EXQ+UXs2>ot03aR+*7Nj2b zMMo(e#7!aF!M&fle95WDlwg2OaoCcA?2y9RBE_f?tXntyPq(VuRpd-qNRh6N;AGKX$SC15u0S__8h&Hq>8*Eu=c5P z^r_K%hGDPR>aZBljJe<_GIGtJ9Xosq?BU%@KoVKd?Ijv@hkkYyI|hZ??`&HG3{RP5 zEmqRU8FceNjL#3Y+61eru73A0>$Ji>5;Pv$UM_A=m@uJ0y56cR9&81}l6L$!L%Hkd zO>-pkK{82N19FFMwxgF(RgSF*6_UJqr@A>I!#4#Uno_P&Fpve?y%-wCOI5`fse9hjnp(COIrS zv=HVxkPoUKEGo0e_*qtqFa|IO4yHSxv|4LZ)v-0v`}=^707-YN4z|&D6tCB~)f{y} zg)T4koBT1R9b0V+<> zgUI{kuxR~*^Z$(znDmegLSP1iQHRL06z(p5XTBhgbR{1#B4U z5y*Zhjn(oha<5_I8G7B#AIMNIsG_oFRUmvHB#5Ys>aCPR(CmnsDYddWW*Vv^DOj*? zDs)|eEScuR!xweAy@kZ&2dvHoG`@;n+v~ZSL$n4-+N2r>R2y}OKc{dF)vF_*n#Or+ zULS#J;GPq7nzI`a?Sqf3|XtofL5xIofoEU~yfU-W0Ca%S`I`3KZR$~3_(|Ef!~ zn4H<{2SNu-Osag13FM>dJiR?TKivgK3f((tUnqGn9*wACq^R}~^ze`5C_^bm zo8yk~zts~Uo`}Ada)V!B3hq7KACW~=^maEg=rjbi`yhX?#I*>R6FPvEKs@utO4TG3 zhx_)51NpMtm+$G$1syH`%?(17Uo5U)pVj<4nN@WRn^44G?K21Cf&fqo0!E zUTS`wFaDzKr-DC_{|$~+5Gw#~eqkV5(-OxA&IfWh3<$_a3S+3X`&US8NZuPMLQ|`W z{bqS@a&&K9y$4vyYqP>89YwWUEYc$fP(C!8ENu!N%lXpzT5VCn8SBBkB-1jao1=YI zT-rG&_w>*aY|x%~9dSZ}uT71nX_g54ho-86M0$PEwro}@`y(ueIouRzrXMvnPyMbo zk=S!g$j9mfOX{-q`xQ512{kTWj!27SQ?I68L?={R7wo@}v-Cul%_~p;xlGFUUY7DI zHy67mCkZrf&mzrwv5#WLa=m%{G2lk1Lll)-{BP^%x-g1%f;zlwg|p)fk3@-&PkdR? z$5h(lE;!|eT?*Ko(rBV_t@!+SyX`Q&b|3aNxQBb`!)!eLlqb z^>EeF*O0Xr`m(#`FRe9O9!uw{Bt^VXl3kuAQQ7Yt~v_8nl1Pt90A9B3>$tgjDqsOwCFJmS`cr$qI zH;cS{&dj_Bk0EoS-)TMG4s@%wzt4tz;I%=P_VU|8G^6A%r!GHHcGZls0kRWy43;*_Y&1g(%utp zr+0>y6@5rF{lTR;)^P+*bCPHw$O=nMlbJy$k$J6z$>L8QH?G3$KpI6{HF&g)uB{Y{#eXAvK7$lgwH3)JLof_L9b+uTmFx069 z3fKt7!bM5E+YlG5kjY9E2xj^ZVsgW}Hg4XK#HSF;!*G@R`o46rKHovfkVthq7B6w| zjK;lbOIx4n*Gihp)fm-)v41UO!kn<4s2^<5l zD2>;(S;{bMAN1ueq0IRCcc?QjTX7cAjt=y#IHtSU0OK)QCA{O8*0!vo@T9*YX;WXK z`1+8aikfyS)wBa^-IO>@lnHwLL!t*w@kbJNJovkzcN++nUWyCfL_vh`)!|fmWFne0!HMf zNv$O%7H~Twb(16GO?S}?(ea0?f)_o~RS>;5OsK`?bUer1Y`D;I8fQgFyhr)Pr&I1h zOg&%+#WJ*a_93l>7vGv{0xS^&MQ@vY)QrE;^}@I~7~7+^*WW;QvdAQT^lM#wk{JY%*=( zRj#s#;?R>4OlYg;dKXy42Cn<`U6htsALlQ)+YR!O8XR#I67Dw+ecJrVKBF5 z&(OkGJwJIzp4d3P;9WPwhc*^%+od~S(Efc7{H|wl-OxHeJcW&$z}Wmn@c$QM(^`VWLl$UyIcxt_Do;p>$a=c|R5?G*_^UDr z2{DSUrS@F?iLIfhzCSvT&$c*7{0Lg9I*itT(#W8r&Q5PH&%h_xmO!{JF-3yp?JK@~ zgTh6h?K#=V=+Ucag@KZ6xH@f#jKBqC-C%R@ z{890T?!M8ZNB|uq-aVus?kB4*Qh0-)gStv;$Uk;lPPYeiZjT8<6oJhKX|o5^-0g=>*bI#dDP%0qKmY?|0iqw}tb z?Im|2%f8emw*h>4bQSkqeEUZw%50G^(fd`+^4zf^O;Ke%STc+NoFfx7EDVH>59IbZ zAeHyFcnvezcrr|U25RemX;vz9lvQ#_c<>o*K8rG zIo!W|CDtP|#-W8~?<{v7u`!omd(m3JX?x*tGWYlxo3xWsgdAX5jBaN+vwq_deA!c9 zwyA!+*jV-)?-C!Z@>so_L!cbI9{UxEbq>|IqgK5|&kPRK>M$_r|1&T0K z1ajh7_JP25HaWc}K&dRGw-J>>d^e>sFBu1kH~fn`)P^UsNSC0sL>GwfC(Va0@ z$~MvV8lPewZ4prJ97S<}ofbyXD}g>l-nqd!D~95IRm0=P^Q4nw6eONSDD&)X+6pc+ z3}8)P22%g^`sHy*v0F-|9 zNKnT*z_)d;xZ`t8J#!`01AI>?XWl;n-L#YZJ(lqbBu$I{N;-@B9l=dK3s;a067dwG zA5Bws{f#+mzQE6xzK2<&LIq#4!PH=Oh*fR?$gs6JG;a|;0_vrB6ET}a@<>(e5Q!z| zMV12qpF)i{>{CPZ!zHx9XLP6#)fDb42=899t8W@=mPgpeJ?WQ7x+bUdM%G1Ti`Qbqo_M}dUhk%Z@uC47n z0(Upwzx4-l6V|%^h>f^RN+5LtR1T7|)VTsnmD~7zOYwX6K2k-p-ZF}MGU=^CxDNv? z1wsn*>B~fxdl?P%pC9VWo}b2o8Q~i&A|DpCSBIFrC6wCsW@P&!BGQxRBl!|v+-a!k zl+ho-!y7gSdsUlgPrJ9VzU#~DqstxVIi-%`(r{nTyX{U!N4zX8dZ!|vDKu|T zH=l~PQLAo&7jHtci2`eCrEirMXm|^#`(a->EclX>n6?Fm2dhb;79G-&Z$(=>7iLCM zdTMmN+JH!eWu`tTLmBt1k@I>G!dd&2ozj=*CttUpMZmXn~pVu$~50TN*$0qQU@HsKfWId*Ly|o)Z*@v#%&6I0GBVL$b9r2jhz# zS%uppU@+Ik9=;vnMSF3;BbtuHy}8N_C?ELkx+cflZ-pff%??uu=Fh}^Ue}44D~#Z` z5Zn3B`I>gq3VRK3gN$yB8kdPF{VMlB==Kr6A3F$w*mOmax1c_xuqA9L!p4kH8MFa2 z0@_`Y)t3j}4320j1t*!s5nL!zmT(I2Nr3#x=$Cn)NZpYiduso&lDUu`jnfUC1=c*& z?M)UIvm05q%1>9)RM{5>Kpl*yj`iQ^?mYhFxh!l&hdHXA5YP!~k{KX#R+?!kHDsbJ zzb*5VkwuGeP1f<*=D|cirbYHXteAPdvj0OTizG!78(OhwY*<@E zLoPl}9wL2nUF8NR5~fGFm!*80$f#}}%6zN=2(0yKZ2*ICHC~tR=IfpXqA*pv*y_(g z3u+D+sc{2+gwI42b;!*SGhll&q>Q6AFMQNIYA9BxvU$Hd5F}r>?j>;uCn7b9f zMs{@r`?C{;*rVM|P|8q3J-488d1kVAt&6F8`JCbX5mWnBb2ZU%1HeyV2Tc83$|D=K zk>*p;zk!q5Xf0CQQsiK-3$VLAGt>5n^9>miARP1<^YD4N-03g~wyD+>^RDJ|USyzU9K?GY@w=fF^alD{p5tv}U zQP6!J+E5j<7)9l}q}o@JnDSI5J~(#ZAGkhF(Pb;vV^{cGIv+H05tZP}ivh+%zLlQP zj;J>00HvgFOf{=G3cv{Ky@JU`{V#2XKFdh)?wq8m;IZC8u6Di&{F53Va{(>-_o^E2 zOW?9L&cNA@MTlb+1l>qn8Dd?mg_iiJqpf2TiTvj|)vH42xS5T!T(577id0~-S1 zP%Z0IevRKO#4qE)-J3{gJl06`bZRa@mrg&m_;s(VgM%=}tgWJ4D|+YAl});)9@*|( zRFG>9 zFIJ|HH>8Zt3*n;J$0yaEf25e#1CTU{D_D(mpt$wmJe(eDVBGFr+qfQa)JReKM`TRb zJ8d=XYqQzj`yJHD;@T)N1nr(=77>uTCkkdZlc?SFsNe?-*Al%b6fneXv(xx^oWy~m zAi%!-)+Ey>KRvrhTguBlCQ{8n{X<<-6?hUktF$a^(Qq=R&!YxpyDL7s{0#G!Ot{dW zy*}-ja44#=PbjbmJtEcZ@}lZVO`cUqmG9$0@888p(->iK5b0HxN1c1km+#>^anO^I zN6BfVrim@Y6YHi~tSIc(E-@=_K4pkn83UK2hNuU0SqQ;W zmJStV$Wk=m83xv;v(34NecdYTMmOGtvflw~tvLhbIxJAC6E5 zu*r?sM9BB&I#jfbyaOnW;%ynmzp#gbq!EHQ%#+UY=bpwPaI#e^wW&!;aNdwW9Dz_s zASJj_1~V^(;hC_Q!6L^<)L@W`lq^5vX&F=n#gbNXW@nu}xz%%SwCBrwVj* zKCEp>+(G22gMiNf!8eQVccUyDk4&!#r6WI^SlC6v`FBOai7LKu^mjRN^+d|(>gJtX z8vsh8d4*a?#Th_lHcA?!q?%4V8?bPZsv$l{WJJtqK7%K0;vYt?z=bARYT9~eZulu#LfDe6a6PtwRdK}qyc*RiH_lHUlNB*^Q$FWrS>QZ~PL#AmM*Y+7tFGn(D6 zH@B~f6eq_3hm!Eo`+sMOH!*b+7UzNdKa~NbmB^lwxHA~_TRRih0p?D1>ZS0rGcBYl zVqI-_cYTkqvocKPUxvpoKKej~*dYld00;Frx12bkpIwaJJ`d|M#7zz>$y?PTtGPHK z{OY8|N<_oYNbq6nRY{3|oH%>0D?=F}(_8lCzE+nTyhe6C$o7O}D_UY0)T7DaTC%hm zdM{=1`?Wy+K_MY1kU&j|C)N9OWyv5@?`0^DXwwNWH@V<$Qx*>sC}les1ZUPH{xySn zlMcnlNU8`Rkre>UW=c5klTzrdSU~}6+%|LKGT`XRTM2sgaf#pxJI%cRna?Z2|H`iF+zo!R8DlPZwn>rzM#r!cGa)I{f(O{Z;r070g{z>6<_{ z$xg4@8}5uvrJ|BDkTfexDs=-Ilh9mOe)Q^xpW>px&YJX-uxbPJ|IXF@r30Q?DTg$Baalf<#`Zn~MdFZBPJ!S2?I zNECvTBOupe8o8)evh~r3z>|Nu^ZMP#b>A+OW?{fk$8HP`Jx_``+#)X=Z;TQe|Huy2kko4@_88mtCvdoqLZ%rl__)?j zE*&eW>tOqpo7Mg?Tti}iZb_Vwy(va&V{wM|ASwJ zgRADu69Y1q^DCoNxrl;q_9-~n$kNWp<I+?by>h|V{lXum_=zZ>vDQ& zphB3#GaB7;8G?I99#GQ*pI$|vch#M-(Mjs+{G>zP5rF3#*tNt$n8q8B)1Czuv1nbo zYwmra-Xs4lJ=CLazV+aR>m^b2VQcvB)a}h}!~t+0$`^}Z8ubOX+!|ClUR$zq{MB@f zYb%UTXckLod}N&5VwP=T1eWS0qfuWLQ`o2oAJFck+Lw8UXXAMMcrQpq?+r6AkXlX* zMFC%j*<^n9&vdo7@K}uQU( zmDp1~^kdIk{#w{Lj;%8y5wX(lTOXDz`!_)+&X@9wBLiiJb`W&*cZ5c`#t?Am7QbUg zKezQro|qQ)B8rgiL=(8BEqphrUTf1I`Cc}TqVY?rR1twzx7k5s6*aAo zP)!do+$@NaqgQ;W#eo?@sh}Kjo2nRV4Z}~kUiTjstr#NlS*9$bzo7`BPIHJbR$-== zS8H?z>1|`$M%oWpl)qq@c0wmZPx74QZY7|{(H(!6$_D=xPacF%%szV4Sx{N8Dc_V3 zMi;QizRHymP!{Fgl}LNpMO@_$H8VSh*@e#eS+35Zr-#-~E6Ki%A5y-F*9MH$#)!Jo zoM(Qio<%{yB|J0zG58kHGyeJ12Q7i~_)=$VwKO&Y z#g%LtxT+pd0UZv*%*e%l*o>5~{(wPU+?d{OMkq$7O>1B=sYJ1GS66~N*^HaeZQ0hD zK=4Uqj(TkL!gFXgByImmA-gnzH4Nu2O#vk7{;-%{0kozdHdu zrtBYR!^l>~`qNe&Thk{3=e~zbsod7(lAhisabmt@#UR)e; zH*#gMo@+m-trvPL60-SA5o9g7@zkLw#eA>f)l%Ol*x>JrNu(1Sy+Y8UIV%;(bhU;k zliFODf$^`Be4_|g-jp<1##Ea}69i_IRdNVTb4%R# zp}TmWFX#eTr4~4&J{;aM8cc9_b$5tK)7>Zn_y-JWe?c^g!(DKV)1J0W|br*{Tg$rxvQrS~t zg0n}dxdg93j3BbSS37Pi;$veFaYB5&yiw^;yZ?lfy5lGMcF3&p9SraM*_X{Z@E~8n zbSFv&$9X!ekp}4Se`b4EYmEtupomxD(`CoQQ#=ZTzd-<@qvIEfx-l^LA<~M&jc7O{ z0;PpXGP(d*PO*X(@i67EMnaou9$<;lXNKNj;8FWY3^%9MP zsaCvMGTk^xmUA}J`apvt4&xh!Jk2&)#OhK7NI$t2 z#DKX^tOu3TV`200w1M=)(ma*YrsBbnUtcSp6RgR$WplTumgV@zKb&OJeS}y5>MG<- z8jEJCj{@IA0uxCMCwZta86{Fxro^ON3_JFHoAOR~xj|ib)=|tSU<%P4U2!*qpDen) zJ*l_~8mh$cJTMo(i(R#lXd=u0>+x&>w5?#zBm}4s1cO}PXfkA$0Tenh_PmA7rXXlk zg@n%@S4*GBG9Q88U#YeL^dbgGW@3a3^uU6!G99m76}aE!Knomn2Pa4t0Mp__wt@KR zQ}g$(_KcVus_X;is*xY^f%ccHcFNy356hEhB{9(J#dtukm^pbCTFUN%wHbzK7n&<) z+=3nN*+Qf159+`4v2CkZ3Fikp;jNBrNb_REbEbN*95lak0B@o19HU{LHucNy;Kv}N z7~eXZUfDHkM#A6k!V4@C_AQqnhO<67Bc87eVP$r-O8<@{-(z5V)vGW1#TO4h>`hD0 zV-RkICI^G?TKygdP?`Ak)_4yH5bBZ+iqxz(z0QRQYiG^jD8kbL>%oQL6JB1dF0eUf zNFPd6Tf>{S4%pzR5fi+0%g{OoUm>>JE3q+1mmI003fc(vGTIs+6d7RXT9jpZ^}LQ;xbPZr-yEJGCdFnHiI@)S$849trF4~8F69rJ zZeXN20YRph!8r>ZjOXh+0$f57Xm%~LPheR^M5a*JkqluE$2&b~Pgu#tKGRmbw*Rzi z&G?%;kpj!HQp((Kmz15Vi_Bd;WH!K#t*KX$FK$m-e)-t(68>GG{@4@)+#z*`6e7q= z%ilQFWzW4Qpfv57Zdkm(=Ug?vy=SQct`pEHMm_;uwfiprpw`Lsdf}EtDbzjh@&1R4 zYH#(bdii1@CJ^TOq`8JQYuBP~xat><>3aP5^)@((+y2ga6c=Vh8}$ny{dhmXr<6#q zMQWA12xTsAICS2HD&75z--;qc3Xk0i1gVsNX9EMtjc%MC)A@sr<6by1rOpZTg0+&* zn6+?t=zA*BbKFGp`lw@DCw>A*t6LV zMX3o4KHSK>aJ)}CkvZ9Fy}lNpH0hM7VfNdcuqNsJ-mh=rI!sl`vVW>x_qZ+FWls^t zDI^XI*?9iYAVIGoS^lWP7S$`RD!fV4!}LF41lxhNs%*x|4C>}al#5y5v4k6Gb#x0U zLqLkA1vzZlOU@N4EOB|gZ2SQ``sQqhI;-=A^7BJr5VcK>kV!bX29qhR9zKh~D|5*m%) zySN}-3#l4pl{<7!JMX@_kVh)jKBGvJD*l?4g2II*;x#wBQtwE?g1aoxd+qa4Q9|QNl_*Jck|XjvCDW z^&qYHbcWzx35?d~RS~1@l8f}+#Vj$3n-Ks|DA~00gaEo7u)Qhm{XiNG%HRsLC!5Yu z3ujEOks<_E5dD!s%wDE7Q4VVKiAVT-m8cr2kt_(ye_l}RtsQ{>Fo}>H7D`3W504wC zWP7Uuvl{X~0U{DhKr?D6aRj7Ry5@;dEK*+?{VC%f%_E;x z4&}Kz^K&VzfZ2S0u0<}h_}b*KEX8YAViYMon_RW!4U8WAAk!O)sId>5bws@}1zMZZ z6xE|+u^csZR@?5VPnHf>kpW+cH>g159ee`cEp>eS00NP||1P&@87UrEnvCxW5IrUw z+W`&B98q9h^DMW6IEEacs~t%sKvf2;{F581pM`QK0b8l9z}R;_3)%K80rv>cQo_*m zOm6@yOjD72mQM_CH#35&S{B5JGI_>|zCwlk^G=Cj1nm))qEg_*f-*0DZ`p^S)<+{J zS3e2DA)T9ltKu3Kkz>ayQg{u;D?rn~LD|%s`02uKWE_(RWo+-3UcjMw2@WDaWov*yH;F}CKbo)eq zzbQm9(3|t6fF5nBFl70ovq)UYgmV8Ds3cMgablU0h5O!n(5_LNrsyaIw)B}}Fx^=z z;jy9k$`Z5YL8`nc>$y&PmyH~`0PM&;!UAT`m|t{!IezN-*F7%lE*pG3a%u|!;0Mj< zj^!K01>Hn?-hiVUL$f@u=JtN4UTE`#LE8#;6Ia%j^HSRtBm40=rioZu5l}ksSFTN# zi*28G&X(3W`su&?Dwu5D>2S#K*UJ2UH|y>M0(~%Dj4zYqTa-1SAjv7h?#1iFJ4i`C z`WO}mY}i#MYXrMbsdMgW@YD6d!8XnQA`3%=>UMcjI@Sfx<=A62&rpws;<|FMP!u@% zR?Fb#J9p%D^BuQ*#&a@aK{vgY;7T)Gi97DwEAarCR`jhClL82FmUAFxjY-_1H~B?Q*o zpS-lDek%dKC|kAHu@a=WCaJN(Va`e1u8;g4P{%ohG1moP(6TO)eDPGBH0)B)gx@Fp zT)Mg{I{>e(jt5<638$XbFM{wE6cbB`FH1WsL(XyJ3W!-O$0?i8 zX>!3I{C}C0;W2uxLF)4Ap)eMxT+F){KI$qpVvyEk<*vQ{|2CHBM>M^g8=4kOP{e=W zjYt74nfOrMPJKYCG$JT3pWg0+H_c|FH00qP7Fv}f>bS~v`8UQMB>PUd2kM~ImbvXg zcZK9-V4u26pDED>c+ysR>cPfHQ&xTrcE^*4a3CwroJxUPxJX#k)7$cvt!s1Qux|q` zYl|8A%wsQH_dEX>-qt?Ct>H9`e>dph)UyB$E{Bsb>x^l1r!G|tFTVh6aN$p00&6S( z>6aE4r@@4ml{kRX>ER%Wc{8Ap$8~sA&vNfmYsNzlf!?=j-pu=6uC3bF&GFz}SG;02 zREQp$;g@ri%7Pc!+BIol-AI!5l9HI7bWi53;*Gsg!Dq$@6KB$E;)C@0k?B1fATXST z7W|OH7!kN8069v8^&c=h8S4(waIGp=kZ=el0$CNm;O5d?*y2#L)x+mj3j>!wE*AIS z=MmQn+tIag_kY{X5ptGJ#_uv049HO?Pb$8y1U(LU6;7bxF|`m1Sqmpt_s@oNKujm~ zSEDP;b;4$Kz(2{LuTInYO(GtNc4y>c98WxxGH+W*pbs&v4kLrwM1*2sBGRN40~b-?;F@r1(EqlZmyF3e+zm86 zOmSxD4z9A@(|ueY4D#aN3xsXlLeL$BZ7h{osVZB1Cx` zn&iKzE{HBIw{#aMI`l=`4OZM`m6DE9B={vj70pWz{16{hQjYrUqt}loivSRr61t2I zXs!26Y0)oq0tV~W2syKEVqT`&qoaeqZ@PES2_kL-%&cQwSSwp(q(kIrrckIt2?{16 z>`(^o=T=@^c~0PmZF=^FX6H-%<`BWa=y$bbcLDQ4tXRSo*LS8zaG(a3 zf9wjLnXrEQVO%k`&jj>c>@vZ_CreO`8G4BwgT{&x>XK=q8fS*+ z>;W|M`zw+=Tcge%!3@z zzEk8MXGe|Lr=+2YF!q;mH^0BmM-GxVIqcg`T_`V}`RK|t)$t%7avU6Mfv@$HjqwZI zN7e1rvT!zN;0xX;z!o9*$cygHGfeC^8j^)3#KUi+L@j0cP|+_U?Y7n5pr9VPQ6{xK z%Y1@hr2PFrX=^dD-am^qDlAj%KT2RQrLtAb38qOKmNMq{XKka@3|+ybf{Ue?jZLBc zKd~dS*+-2~xJKG@Ff+Lb)A+cs80dlA7l7#V1&|F<&O)6}7E&Sc#L~fk!W#PVg!dTe z;i&^lqo-#@P}J0P)BLp?7ezw7^XT%?7G4@zT$9P-SinE{+Kz)kTJ*$}r=Lw1!{@(S3?ZAU4ba z%p%2+x!9#vqrIU^h}gGy3;Y$-c)>rcrBrnuVI32AqPcM)1@F6gF!-E8bGKF((EFUy zm*LJ3JM-mnarLgdlwCG~cg^UIlwt=OB{7DFn~3ZbPXLRGAliTT(;4 z4A5t&MlO3Qg5<-TL&jzd__voz2Ef!rAV>jtdx-?ZJV1u+B+7_N@QCGiqfJLHvZP=U zcvg4NBN%mE_&s&(upo>DHa1SKg9}-k~?VT%X^6;!1 z;)8nzAns$>4Nu<8(Qf@?sF3SZ!+^@)0bWMe<^RtWLezVLMmxRH1{@W9hYl*JbCe1U zyhrDtxwQ11VXqoc%{GoQ%8{;C#r>5F_j7FCNK}rwxQZ@}(rgpi9t~th9(9atjW1Ao zz;>2C`~P_4coiB>!18%s>XWo5Q49CV@_ph4I#hM( z>NCME`c;_RUx!fJv!j!L?i3`e+jC?yEFs_cZmn_F^meVru9DJHKLr_S=Vr!YI zuQMz8>l!6~Dp>7&@&@E;ojvzm&Lw^JWv7I^F;@;bH+5Bno`J{`?xt zJe8LOs&(U0*?pY$DR+C4MA}wGD6(IYfBsef4GWhaa@pym<;Ls-?>hO+*)(-MnmF?_ z;n!-Fg@XB*^{4}7xY2%fmIeWA#rb=~BYLSc8E$ZX@|tjXj^TF-{}$3!ZN)ag3vJjs z{V-N^VHsV`%^n-SZOkAR1A6nlAQDTCc5^=l&6D;sUM+oo4!uL_DTkoT`$KG4`KT(_ zL_pI_*R8tx>#GB$!RQmsYJ}8hbi&b<~(=EpYT5n!7k4zV5J!$J9iC zHy>lM&k!p-_0U_<>}(EwNs7R!K6OlXDaK1WYxCc}B)GFIVKX)H11)_T}p7~?CtVJmn# z#(f=*HT9w|DvGEz)hbi$40CFGu#k~HQ--WKkM-W><*qowEuYjTiO2pj?6pHMAxC_s__bB;K#tw@TcJZ_9mR}c>fazb8WC7X~iLb3diMK+{*&WPriMQE*ddyPDIC3dZ!6 ziVuUVa&>it<9V6tn09xm@HCX<`&f`YyF#WhPEOD><_UC2&|h45N+3TYmvKlz7Yp2l zl7K~x%ns8(nXH0%yut2ggKk~0G4TNLIbCwTlJ*4whjJ@03iVuli(2%!RZx_wMV6@p zvB);8wI^au271d*4b(c$Bn0HishQ($_qh0)7vhNIe4UZsP`OOGXX}4C?yLdup==i@ zC*5Mcxsf(E_-w5V8~cA`LFM^Mb^-3VrZXJ&m&sL#dbWi`$21L}%u>(S+8_mI+;~8T zOxliWJ$bjLW`0k&XY*OP-5l-0%56CYr8KC=ThVDfDs71}`HhrUf@-yp%oU}udfbBK zLrGi;OH-&6+t&yNfh019UhfbzG)?2cuI$xWY>UC>SRB4Fy`g+e%6ua90-4|HE zAu|wBDyvj+W$dh4%->!4<2(kdBeh0<{&exX*4Ijbg=wNZ7WFuXyk@&TdJ@+#uY^H) z66GU+-|3#mOelV~i&qkk&Vx{(B^NYOZ_eRCFoSo0tV;=XSWk(MQzhD7;*y z-ARAe(ee979Zk$3XBk7sv~Ct|-dJ53R+0Xfd$#r#p^Sd=Scj?r&Hj()?S3Lq!}-A4 ze3|MCBc9-CO7*0fH+HEG-Qbii>*#dMr)b7e!b?eVYzGBe(ws!s%9!+))H#m;VTYUQI3hI#8~(cNIC2#Q?gL z8CFq-|39XU#T`mGat|#JPa6vrHB$n!nR}rNtaS>^B^xMF%y8ZtUocehtBAJApuY_w#< z_;(X5vhC?y50)d7$01r9k0o>y}#rGU=pl7qH6FQq*4nMgUu#>_t%@Zu9i*Tw7do@bR}a z|B;_o7lc0o?liC>o55s+t}zw+eR!&LtFCAwC61d^>T#a|_<*$D5Mx8O*txu@$~zGM z&atco6c68t;oknk{y@~nwRVZ<$EY&iR4qLP<&5em^0m>_&i1j$#lJpo*q#bWV;h`? z6_t=gI-rWd+11PL1QxJsOXf_GLQJa^Z;o0ZkMGXBAX?GX=@mPBjNu|B#<&R9MCO+P6gngc+)zz{UVNmDKaUZCEG!;%f@E zUSx;7hkD#DV%BObvKZALv~iSrub!|8KuBgmlHfO{j4rn8 zO3V@dG8XlLE*yJC!I?GP9{)T*Vo0$$nUWs0zjK%XQlI?StN>@Kxdava^h}+}6pnC9 z5(j$wC>;5&d;9?-rZ~Ia-?WgP#^g7>$f}Gh5`X`SXXJG$Wx&a6TBdIVc#pVy(L&_p z{{bO-T(-;G)X%YsyFZ+c!D!tekm8c^gBd7>t2bwj&TFE|(voV*0QiRs8-qtZW=#ZB zW6fBL8sfr$PwnySm~+V!1*xTmh@V&JY9s03-78zYf#(|-VDk>*X2HE(=RV-s;z1S342lAT)705AVXsxrn@vMI{y6X;>kLHFh&HlhuR~XO0(5 z?Wi-H^dr{8Vtg}mHYO4C#jK6B-l(!7&?40Hi5|(tg*wK8^WtMd-(v+=+g`73n~hDa zJbfTY_8Kik`Y;vj2N&9(n;(s%dH};6Q9(M>jB_f=#K{B00WtfAq(`h>w@xq!=KRBk zgtT|?KBm*jCq_iDuIh6K2={mF_>*f{;KBubh>Z%z^^XQmF^*Y~3}->@?tkqn&-zwO z;>W&vTP8zCTFe;0L>&ZJ%c0!|v1F_=?KOm*ST&d?dA$Fs+CkLU)E}{MG}zBcGxV!f zX&cpl>45k@@UMN?|K_O01w5U55XVxJz`7(oQ>e1PXJvdEGpv}dzNVO*M~MFgt{3OH?5D-UE*YLu|ovmWe#+6Kr--ALPCTWz%2`HhIp4LNm>ay&mk zp0gwHcl7E{yK)`$ydYU%x0fu%z@Zld zbkOJ1BMH_hDR_Q8fb?8rrFxHwk+EFvC20EIh2eN0(pdgCv{U0sz=g7rgiRd<;}yL= zzQy^Y)=d6}c1G`*(t+>bi0W1?@zq!OBX-}@Jfywh?P-`{L`c)O0!kp790ckng@%#Q z?k_0rMkO;@v5#WUhiVU5Kma8@5|sUjh8KCPzMxD_yuKgvT2ixD5T*J0laPBgl%8_+ z3LwURcitrsXshOynZZ-3{C=vUs#kpu@D6%@xVg~c`(iDoNL{fzUY8!_VA}X}9!58> z0HKPed_MS^lQ5`MUMRa&7Ob>c2Ix;=$u}4a#pnD$u~>`Md-0}0mY$vkG#12Y#&o^i z6k8-&J9w7ZbD^qMQ41JzEYw|%`lmJpUihC{BTtn$inhl+G$($~5s(@w$y-)*9QQ69 zAwmU>B9gNA5DRc@jBB%3b%b~P*g{n-)o=<;&;LxIG8V#Zv)IJlwrKZroP&m2d^pLd zsJ0wL*9l*drx?r2sBy#&{h^>ZlKPLzH`Db*OKmk-zUmrBW)@0C(X0Pg&>X*muanqU z7j2K4KpjZTpEejt-u#QG|LR{~l!#J$PfnF`2I5Q5S>xdHSq4X#X-Hx3D5n3s&tIkM z!>Cm>NBFPA#2Zoxg!S6eS~^{!m%zwCa#5yQ6gy1qA_ymxqRPDt%lY+lZ&j;s#oF6X zP#KSunGePXJ`rVqEwuecg5-omteuw~_*b6PkCmVel;%$tnNTwXkzxPmRdJvm%L1|= zI>Rz_#i)uVn9P}F-?c6@-PuHrz6~cq6#!x3-RyXp7$U+6NYx(qo|7f9@J>|OJD{V1 zK|v5kAJIiWpAU8O=~P(2xp#H1WnpeHB?_%RTqHsvX>zv7H7(p;xxRJXxt5EFv*91J zJ!N21k7v%z|D?xa>a9koduxhWLi~DOQlch(BEH;k6<+dRYdP~oDBpm7d8#3s3i&Js zpTpuYV)ZA7(^Y%DVJu`WpWh-0EbH3xiCjEP#T31R_d$z4J-eXpdc)Jw#E(E-peWFPIbOPzXK7j;*UuRc<#OYnrI=BVTn%59h zT8LIv190R*cE=$w=W2^R`N1Ul_*@10ScxkX? zK74K0PyhtPGeSq9Bn~PDk_i=MO&GM}K@Ok_*|DNphEQ+cn}L#K&SFv}_vrA#4{^#e z-!bM2SxU@r&Qwi4mY}uS5Sa|cBp(4R2`NZ;fY@7hV9UOF0@bWG$b1`(nrT@BJ(*Cz zxpN_$m!rj~#5y0y2!%0Tj`iV(ygG#9z4wn$xK6CH?{_P#R|I#29}tP-EFH}s)@?G&a}iFD?Sc4+JL&4STqI~a1Lbgj)wkUNZK=*HuM46vC! zFyF5eX#_b zt`tizh=?kd4ILvcbVsgM=H%*Nn2wNS!|+TCH^UR+&agV>2xU#b4Z<}P}w@Btc1leaKpllBP3e!B>=3vJwfSb zcTzVppPTHMb9Vfu2Xu^J3fEwAOI}Lo-j&_BO%VkYe7r*}Vs$ve*0EDJ8`m1G+#PIBD3Ou_aWJ4pPUy7yUu_y`G{SN59wcK~1a!gJ zs+EjQKYGwg#g$tJ4TH3-@rh8?(`UkXAtD{ z?)s{esGyB*_=~v>M$F@MTRxFGU$3eqZ>dFhAnRn;O+lDEpj@k&XOlKfk;yJ_n+XPv z-h54h|8O&m9AeN~de#8-rtx%2TmZnp+YQHCORaGKyHy0e&{ty%Z`lB&7_U}raj~cu zNQ!LSVNrXzD`>|ex#69Tzs%@jPO?k#o7)+;8wcvds|JEq2u${po`$oCX|jsqhDdk_ zyM6`Jm)lwC`jp>eINSQB*nEjE?F8X-zVKt0i-~vmQ692j?{wgrT7j131U(wZ`l?GN z2#{=aW6X(aIZ>aif$cSlWm46#H0qg|M^-T*YyOZ~QDb^6Eo-LTn^=h%j+VFKT-&*e z*}CA2ra&6_jp&L0-JZWK?7!?$nsFm_6IJJ6!Qn`&O@3tf^14SogJyF?wAHv>*$Afj zAijBShvf;Z3knBvqEBo6?*fRE+1Mz)U7Z@@Dixcz7a}-s^veDY&O^kfb7u6KHfE0q zzb;g1E&s(F-^BCN8i(wclpt3D4yPbvc~FW9f)WIet%|+{JMOdmPj3OIVI%YjcTz|s z7D7%4UM*;pe?~Ut`7!8_b`RrMw@iG{(6XeuHo4y&oxzH-1e3tVs?7)YRYgWfwL7T? z;7U1VxL$30cRmuG9;3Wj;2v@$LDI)UleE{S!ZF|nCns*3b7g_(akT>qZPB5v0yx~3!`2N@0zlJch7f)ciSRj#gy9j43kf?|nmloFt$ieSB-|A0XRh5#zby5y zDx1eV-AP8kYHv-wNoe~dMN~Z@(`Hg8HRy3}VT&_W|J$owgLNeIBTG$-4CgENqr1rB z>D<;a_-J``a$WQ>w>yKrIr&uMDHIXt*j~Yz)O9P*WP*C^WQ_Hfs$PLVCF(Wr$gEP69z<#0FI__+2 z01iM%W^ThRlG<@Jh7C$Q>a)X9zcW0M!=6)+y4{-1HzikCFX2+w6u++4#Q&tt<*jg7 zoY%3!I^aOnP4YyoZyd_CRBI{d0uteim2xk!gjd!v;YS!x5&J;@+XJRfxAC{Bo-bU! zSPk=Ufd<;Qm;d|!^$n1hVxmNd0N$QG6J=?WXhD_|1}HjM}T143A>E#&|%N?2C+N)Q8d$#k)0k{2$8Gd6};o zK&;J4(?0;VH_6%rXWu$bLt^cI+x?2d>)J0Pb3x_YcXr!8Kv(`h~$x+KhfD%%4NmIJ>yzQlNyF(k+IazjC<4Sqoh1WGa&Y zoq%hoF4zX4&rr@bqq_DkwT|og=(t62 zYOrGr+X08HfPX#K8$6Oc$vU~twc~DIHAlRu*myLWKQ#fei!(alB_Q;J1MaiF>XC^7 z+}~BO^!Vp^k#0JNk#VjeEd#XR-n=D%%Q3$tb`vV|Pj>zB7C^49nL{XN;_w_8cy7y_ zejJK~NlZZ{+6+L6_Ah3wvV}(K_qXt{$>RpoxUoDQZJfI+6Vrk0%5M;Bi zk;p{imq9%>AUbfoV^EXAfVb;m)}y>e%s+%*b}iD+*Hho(&07l_C&i2ln+B$B*j9h}9@}gwUoTU#r5E94ZA?dwXK$woa!bh;1!q}IM_Z~n`WGTJ1*hfK} zV5kgE1E5u3aQklmdS=%zj&0HHlu*WJUL8@zY5qD#otM%$)4U_>U7@QWqBQKv8{B7j zTTb2Fl&=-O<_7mr$vx}=;wHD+F;h!HM!9gt5?xTBPxhCBEp_^!*~5tf2YnH3xLUuW z`Exc|p-px$gins^lJRkSP(L_db*mVe(HILc8PXU(LwI=DMf+yn?|F6;riJ_{oM+-n zDDiQcqtGGKGg|R6fIvS0C~-D>Rh`p>ZTT}-aW(q+%#>Ht4w~Y^M%R3dEfmyqngC&~ zu=00F>*(a0GAf)l=^>Rz$YTm@CUWY5HevZ41j!E zYV+vFv+GBGnQLXDAvuJk$V0};Sj3q_xCNgyD#~oAO25FLDw*a@-AT9yW)%m=MZAL0 z!nF+5l_u}c=_tu~HP=i?=iMgQXX6y|9aF2f92!S{()o0QTnL8FqtN1jbuVlYb*s6g zBk^*6N0S;$*)e(lwf>tW#0v4+$#S>k$hnaivMFz!fPjsylqQm5Oq@C2f=>2pV3oh@ zocEhin!IwSqpR0@5Gja5powJjte2b3Yi;3zrd+WYJW4=;9D0}vB>RNB6$>Pt7S*}d z++K;Vzh^IbhGZ5HDt~fwW!MC#2pyZ#M0h9F;!P)1^a!Ti_l5$UK$8NZvp;4AT zVUjigODsasf_(j&@}{1z>FF3|EjG!}>mc7`!BSnS;=*iJlaf-DvG5wSAm$H`Ts?yr z^?3_Bb=q^EgQC_P)#y4n3vhVDT^*^rf`I;e=6*PZgK8KKNfUWyYS-&rWjZ0&@G-! z)?xvG`CM!YvcPq<#_~^N5tarJT2X;$Kpr8RFfez$F?-R zL)tob$H){E8qfF3iNhJLobIR(30hD#6&#&i#$Bx|8Cy0!>enH#-(L?~=Y)Zq{L4vF z%IUQ5f9O^DMGe*9kaNa|ICDU}n81JvxmZ}xVL3RErwSLTFA0B$seLnugr`D>dPN@otMbu3k*C?223!7o`!vf%k(j@| zN6KI5sH)DtV!No$k}Kj(fy1XwpJ8F6NGIaCPcd68=*t205@~3Q!*H-Td1Sml23J9B zW(X^|EM)n#Gk~-}SQ{op8dcW*k(77lXrXsU_@j>IfpY}AX42@wJ5>h4obVE|2K~#^ zO|p)s$JN3L1hYo^yG`~8HdR)M=1Q(4mF2a`_?o#RcCpttnrO(j?;iXR@#vbCJk|I+ zFRc{E>NTZkrm$_!6QvH#FzBu$%pHLa5}x=Cqud6rKEJ5N0(l->b!{5*#rqz&o+BXS zj+^RN5!%flS)eHi^Km#`7P=2F^kv8c!q&M5$*X{qJqFsQtX=PrjJ?ZWwDew`R80iP za!}4w5aCG|B@uoGfTxU*3q%~{&=5UhmgNDBZI=*jdFu)uTiW1Mj5rh*CQfGV?5lXy zDI*{@vsg&a4B6C6ox{y9nw{(^*Ns2jEc6n`$3bVx%7_qcY^AQz@LH`6KxhHdhS97) z(CLD0JSH$^9vdq{tcUS_esQd=2*-TW}T)= z!$;^mGBif3jcjksx@0v{Auw;wrpsDpDtBP;S;#<&IdQ(;VqDsDE6d5<#RWk3uo>WI z+(H{Ob}>5Zz3^f?FI*%#8iT?K$;P`@9;PW%M~nF_w|D+2~^t_+4*J96M zH`DwycAW^jRV9S=WgTH^JGd=WNFqz=?~d9t?;B2c;I7O)AYL(ACGH@QIIT^r=#|Dr z08X)Wv1km+g3}92W&dx2rhQ-XM65ya0k!j9eg3_nq6F5o_*7lfYKd9PsGH3EjHVGg zhwej0nZ>Al;I{UxM)SLT@#K-sthq3A7L$79~D5o?{i*0?U58^K~*46l0yk{^cs zT4`JV0YySajQ(L7p?*9mSG5tA^bc|7-v$LvTa$Ul>b&p1n&{gq0F}CpwKF&v`*BNU zYt(-)v)@e?q>XiOKjz^v@4F!Y(~Tuj z>{?#uDID}xf&aG8wCol|g!0F5WfO6C+O&C?w7Rvr@8j12I}`ggAV&5zk@12TlEdzh z>3sn-xyu76rcWNAmfx9u@iBVoPBY$_H~L#_O&$Y_*xKb`Mj0Jgo|qc%58@{`Lk;*> z>KO9uE;anMr+EkyLsIRNaxD;6OaP5(hNOVZqA~c4&20j#f?zj_WcUA>wjoikp5}g} z0o!fV8rq#R_B*~p=C;oezFpju^Iy-h4!&>P9RFC-F`d@L8k?>+p=_(lwAwj`-3c%D z7jwU}Fci#J4V@R0HHzN|^r@!*(Th4b)+4U@t2U6V!OkkEtU{!Sr?-Ej{v%1clR(%h z@hb34KUV#h8JuNS#i-#-8Xfy_v$JKqk(HUMMY=r9y1!bV zL&xZkn9*LW%Q@bpg|hm3WvM~^1j8e4*nmLaHmaF?t89n0NEty{KLjb^l7`15azB&A zOD?kOvvaK&`kte*^Bx07@GS465A0;RToQ+jp&cqU6;GNdnG@5p)sq}ofRX?`m5S1H$fl%geBE7-H9CFkk`S&;RvK!I#i#+*>P5FQ zMJ0mwBd=ebr)!0*83S4$UZv>@o4rhcvCzfiIA%U_+1%;ABFG+i|={Arvx(o zjv9q@`1L!}5DJ{+^5E^hT)ji8*bfTEumC_C{LzVX+KR-V3$T@<;-yM^ZJwD(#p%Ge+_V`K2LGvWenLyKqoW4Iw}9ghJ0Kh zPAF=_Qcs7_JpkYU7kg(!=CXuR%r2B7Ja+MWQ|!}N14!neW2Ir~;y4Q$&{SE`XgL1C ze@og=)TRMvlK$OW+Oetg|GwkUM~WIAN*GKNW*6Mur4I00H#z8>mX7z8{02%%+xdAA z^q(Ee6GhXQ0L{ax!U&0*nQYWzS7izBup<6#Gcu1n@UQ7^#H z5JsR~SPn{D60sMzh@hdXaW}UXYNB9@ayq+QkxeOqt+Td0>Lvvw32;A|H-Bj4c(Zzz zbNhEk>`v*koJE|*EsPb3pn>~%jC^ZHELbqwRUP+pw%W~r}~7K&D;vqo7($nEr+KIo_mhNq9}85_Bk z@?iJM7$J6~Yf^O-Wmc@ir%v4i`}vvK30+%O z^sOQ$%IRezD25M~FM-@k9{q%zu4Woms`b7>B)?L-x{QAH2YI$9Y+Gc8IWM5cSe46M zY`!K5C2|v!irckI#rmfj9`q+?Ij13Z(Uc@E#Gfo%mE-ti#Acn<*otFjpA}-%itP#h z1+fA8YK~XT3a;mNCokJk-PU&q*4CTUc(L8PNg&KFrco9?f)vs5TsIv0lFw=i#XP*T(9b*RBkHoqH0YMOz{~a*B!D7%U?}kc8}dkyicY4J4EL?gYCLz-07t*n%_el(iZ~_UG?Sz zH7>A9|MqSa#&yS45&O^zmRs@14Lo-emE9I~GlDy*)a4e~MnHV*$)hw+V&r;x-^<`m zb|U%-wlK*hL9c6N>Z!t?^2V5mc&`PD_SvW%L zTT&>eK?E%nH7uk^#L3jei7%=&2VP#j$GQ3`Y3Bm_ddK!Ql55-^3RVcdmnt6o16`4# z2jSXsQ(Y;C?YI)g&2Z#Q8%abWmL zlFKG80W7qFE4Ca2iun7>yDi zoSNLU$cJIPT#OmFHSB1qn&>teqqS|g`EXd{PYkq@GD*hj_Q@{P`secpRKvTp_CG8V z099WLk8vIQAV7as7ot{ZZ;|kP@7wX$CTfX~;WF$@vb+*H$}jB(8z2V}-|G^HSUHE} zvssJwq}*(%=^Sy68MWA7*PLUAD#VBG0) z8kE=6j9i`2F~csboTaSiqYM_k{=UX%3R$~8E$He|`$upE@;4hc<99A0*r6e)JqfcJ zCrL4HeOivpUaLlf6SOF|P?-fb|E4j?I?# z*)|aVsn5IWz9a`zkn^+la34Nrs;lzh(C86FZd-c7?8A72A1O(GE@dFVqn$L%qt&y# zB;q_g=ybU_Rc=3Gy80R<%f>Oyez73v@!Mo&3ynB2k32p0Bl`w=89&txJ`n4I(5s`O?LWs7E$plz6O z^~OfG8R;>$R82jg0!rUf##o4R+u$`c6;g0T3vOevTnOHsBbVvP*O4Tm+A(Jm-vj_< zgp(3m>oI6j<{A`sPuXyHQMn6DOPqBwPro%)gQqj(xCE$dLzUlg;rm@T-ar)@S8rF!vkaLR|PE$WVKL76XfK}`q1m?z0r{<qCAU7h%>ZP>dVoSGHyIl5u>MBzRS3fi!h1T5CuE1~ITl1tD1lte zFZh7^g{!aB+4-B&ZZbYt^sIJxtnksimM9=~uS)fU=s|vG^2-7a6W#_R{Tk?4vV2{6 zy&oWrvpBsW*!*l=78&+24dBFiXyI*?eH1ot{zfrviXK}1A$3M@^=p*{gM5&U2?!kjB0V9*aH zy9KAKbui1n2F|gP3oD++X20W1-{rs;ICycc=N|~R4Y#z%n1O8C|3yQ$icV*t__X{m zxoWX9YfZ1qq+F!(MboyK?waHC-mORl53<#OZ1p=hls0zJ2?MZNG37Y#`cdu?e~xpR z68ab%FYFX_QHp5O$^Z(9iC$@yG2x*Q)ey{vQp+I68w7u%e1-X8Jb;7Qw|UR+HqcVb z=_2-$-d=&exM+?%?L(X*Jq1#&tyM-pp+P>`vfv*^pVKq^Mx;reeXrfFug1J_c=QtWpi;#Ikf z@580Fv^2&S;|jU|C$dX_tDCTs6;?xgr^<3Rx|@~6(tv$?QaiXqiY`SEvQ&GLRROa_ z$jV)qOxkXl439;ASA2X%(B>RAo2&gZ=XJ-21|^3|mm;QnMQ2SuJgf94s)K5vP&|2J zw{OV52mPG(R+3#ZrULyp@J2m%=yD*S{YHT?`{+Y-ZFdVdsrL!l6ydt5W?qL-TBr@L z#)iy~!kLix40C(hRkYFR;w?E*ui}@_Gh5?vwX5c22{hXXd&F|mTghFPDyCpCJQ8Z2 zXkw(360zphx8#Bv0IMRIC^|Yf<|2iRtnzSxsb%+ulWdcznFeK1>BsB+3cS4t@%Dp3_Gh!0I`rO_WvDbpg-u-1;c3ja-#$jIs{*NoRZ29^OaTxw4byx-A!M+0AH|2LRFkq6o0bTD|X-;5# z80(WzV@my=V*7NOoo;ivi(_sWx&2)|>t?R4(`F<<(yYrVGy4_Xs?_0@cbN*3Vq};b z&nVMh!23mSFeV4BhJNK3-90dfO6mS0q*4~J=%ost;sd2;@Sx$(y(MLPm>gwRL)F{`_-&;e8c9kxkbj#04|Ntb1(=sAX0dKT({va$Xf{u8iH6OAAG;mxld*Gx z-CS0~{MUI0Kjpqi&RCC3MI=RCPojwP3qLGo6`Gn^IXPEdmp17WqCd55*Y*Tr+px7J zc6YE-`udtLxOMX$l*Rb5nfaafR^Q;W-GX>aNh{nu7E&Nm;Gk&79r?(K+6g6eV|pYG zlg_fP`UD%cE0w=CZ|}|U!}UOv_*3>1Bo}yp^oFQ??&Zyl-0l1q+yx7RcJEl0(8p$6 zMeU862saFZ)9X=k0?f`>j#iQQ%4#bBTpha|IUMO2kbjiv5G7}6dW~%A4pW+3Q8JIBwg9TXTear^G~QXQ zuZlwTX7`=YW}baJ<9$QUVg^lGX*EqNHAX+EN9sKIuxI<29~0C799O1^g`na;D<>#) ziU%NYzIvVNuE=ew6OwXxP_%i^#wAt)O-@FV02w6V!tiAr&y(oC2B7nqaRFk)zyP3m z8gj6UBr9#2Eq(#fm{bVOX#LKuo!VZ<_Ccl(G^xcBSkgqV55ip$J@$m)>Vaw7;9w(t zzh;Qc-A7oJ%&iw+xeD76MX%xZ`!@4RYylRo75hE7#HpY z_RT+*HLQJ%fCD%~LPXBJxW1AT3>ML96U`1oj>+P40?FCI%#BBy+SXKg;>ZZ!(^6|p zEN9hmT(IJf4k=(R&;RDRZH;~&Rpl_iP(N|`<@)GkJ{MVm$-t`-#d=QZ(9UB?&-B#aY%2l>a-MTlm%|K!7+-Uwp#)0_#2C=vm7WSPuDaMj7d zA2_rciB}0i5QWaQpc{}KKqV8hOAFyCsr|77lpm&#AGX46(?wA3dY;p>0E~Uc0;Z3N zs$GDF8`@*iH{gl*ukW@oqCdn0?0wBkWuPiBXXQc+WEOJ!DD4>UrK=sbXv?>rm34Ql zH6h|fr;(js@`rY!YU37iYdL-Wv!;B@QP|6AXubFJu%&_L0Bl-&@5;#h5IJKeM}(e} zj}&?GjzLn%8EXd(CWnD{-VVp!KpC*=HQ@!pd~o^rRTt6`2utzJsb>$@nm6~A)u^22 zc^F7Wn8uy5Pe?E&@E1dRvC6%Pt-j!!H{U6Zc#J^0cSNQrc!RPW4w`D@@2+0uOMkgD zzfp#Stc>Vx99rWH1)94h)7vwDwe?2Cl{gmLt-|W$UG_s@TDbfqv&gR*x~I2SA;oe7 zPjji{YyA)(sr4vDyGmLkK!llX7-AT+5YUSbZBspL%kGetSWEvH)9CUE|zISz6WjPK{$cV z-vBsmONpXS@t;vdC~Di`BT?w$ZLEiOIl+&Hd|%Pc>B}*rdDtt-<4$FYS)U6q8t({# z7k)a^f24kOb;F5275D6e+ONFP8l@e82er=!c`+|E{ppcethPTVX+v#v)LbDAVgRUo zef}&^f4afk%+V4^LiotjsXBjYQt1YO$H@IuscXlr?;*>EPhNMi)2nwzH9l!CqgBmo z9LSunl6snRmH;x+jF`G)N6B%QmLN-l^OXgKYY3AK57S_ypf+i`N7%}^@t6yk%U(ed z-)I(vaI5r4quiJ13Ao_bp+AzRYDR?;9RW1Jd)UP8jY0^@_6K!h9EZoEu%fa2J;b}k zFkt+`NJQg^J7lfKc`j0-@+@6RrU}+5*opTRj3}Lk}`^171Xofi4Ga#QNr(1b=RovT1_giy9$K-kXtca}gc2k}%Ki(Mc#GaNzN zpFE_8Vf_p1Sq@p2(pnM&_CudMj?ijCN3540BYY6h$^HvH475Bv9~io??KSxYqN-O^ zx|a1&0cVuY5$Z46{eCz&fI28tT5r{2>yHU=X?#Gx7v;_i0&r1d*=g9c?4?tr$R{T} z+C#`6SjTB|Xw4U|`Mfpc3ZLktV;8-Sgq)-xY5RUCsvL-^&tnfqgL+*?8ihhKC$NLNRLJ~ zhRB=)n;+a3zI39h)TSiabuw%5HFi7aCl20&1_o7GS=*Y}vE(`#_Lfl<4d3nrfW(;s zw7bQzd5Ta6jzH}>h>!1C_=a82Y|^dl+z;eYlUO7POVPM7MCP^Y>}ZqvcyqBu00Th$zt?cI%I_gTBZ{74!l+$$P6q`t&;aO9 zKemdZqwtBPNY$3pEeUDh@eB8u!7!3Znm~ZMiQ&`W2mUZW_txfA!rnE}oP{4ay>jpT z_w`L4U?4{h$`-IMt01k_$7l$!lQU`*an*R0q{Dvgu*%Sfk`FINu`l^kv`PU1NP1j3w&#OU__JwYMH*7pA zqc_y?+0$meB$JP1*bo0AVfud(uQI_y?;!S|s((Tm4`2Y$&kIe)4u6wLGr^F8c{AP( z85Zzp#Mw`s1^6oWku}nGpzxoLZZ^|42s>W6AkG*sND}WPF0$!1%+j7RbJNyZ>9Ysd zNqrT}RLo2~DEaPWNhL*lSA`=%og+e*!_%(9~dJMt%;bc zTtTp~VM;FfsC#l1B0Iw?J{AYX9kFeeRaZd zHzUltwdT8#5TUKj8H;(jm-mvHn`=dDvWArImw-Rat0qKJlEdZ|J8ZW%|#Y(TBqGoz*ZegMgkw zk|H`Dp6l!^`lQnpzMfNZagr@GowB8ht*W%0J#zUmWKG0CN%&81Nk{l&JF0dd=^Q13 zB;8s5r%Ds%8)r^`|7TI!RKj_3%4^GzfTe!nd%-h3XoKIkV1Ap}-EE$;HoZQEi}@|h z{E8OsB#tt5|EAUsX?V??IYYd`;0gyM_<^_ObIyEqRXR#!Il?=MM)=_ejJt`3{dr6- zC-~em%lUNp2|ng)qeh;~*Y!Z`Z=0?3MyVSMBNk5l}9H%}@%?nt}8R1CjA zcwBmWL8a9;CHG|ViWU-R>6jeqUOrhIPk3`ge^g>)Uf0vGQ2d^*m@TWaidD3+)S!382KZhQ<;R9;a zG|7HP9aFaJ%|+w~9q2?v(OIPZSLtR&L`@y*u;-LmY!l=P70r-6aaS%e{NkrSVmTT5 zSmurWaTPv9jUW>aNxS{9MYf3x#iIXYoi7D8C9L=^hv(Cl`jYk8-Y5vnqCCgDk0$|0 z9B`Qz%IeVr(?Vh8CJ|xAJZ4W$j-WEK1Dw_r|4mK1&ki_9$Jum1O^?s^h4BBU)gx>) zAB)^*{j_TmNA=eN7{4eY4NVvsMC%b}AwURSd#Nmv?&A>qW0GeQwg$;wuwdb2Yqaf7 z&;If{tut`2x=k)8e+l3eAWAjnm+- z_Lk!XC{1z1lOuq(JeWTF&tV4=PoB8$H}R?rcVpmSxG0H@4Z5aQjIcw-e@|#AQoz_Y>xL3~ZEzLe^tY5( zC2f#vlk!9m`sgdbpji7dk$FyfB5R6NO}IR`CW=|AOjqG}N_7fd6$>}(!oSN@x94xA zLYIWzK>dwq0SrJp;m^`|b@4|9xRN6Y_l(bd-%pCdm~P)8b3b@$N&e|Ys9Sk0YSs*; z7n!6Hyw*BpaxsGgSVivlS%ku5`D%auF%sbz15Nx0TU0l&Ls9S+KU@Qfgvr9AjOQP{ zql9{_^^foBeTIAwaRXDmLbUQ68^%YFVChEYJSJU|mWm&m__t0Ymq-->#hEPqmE5>mTo~3 zc9M2j!o`(?{I6fBsh(iUWAP+o3u8L+k06!rpw2ZpB(R(HI@-BW_erZCo8Vy4+NUPP z_(WiLVP9FkaZ&Br)5s;VkrkXRzMNnxt9Ctq^C9fOclS6HKt@)+YgP)F(dp~?&kfXj zEHsMY0o6~*u|D5<_Ip8^V}J)yFSbPsA?7phUtY2qD8o(bGvZ&>4^M6qw|ByEQ}7Fu z6+N+DB3#C1vttzj7RK?70OQ$4O6XcO_cY<%sd#fU^J*`5PaiSWq38P=sC0s(i?3-j z_b?m7=xr^{0aY~Q1*qWO2l<#6mEQy!G2RERCmmKS0$;RS%rN`nPWL;dRtVGg1{q4F z4&l-w=Bq#lc!-^w?pIMiDFQcYb^aOec;M7spD0_i74_FPg>A$k4)EE7n6{w){Z(GP zEYK|4{pWRDo-l;O#;L(hKL;Cs;xY6Git$m1W7}S}&rO&#qgf}x9BFdGQsdFdo|RUR zeh?)y8py@1`gZLMy(6|>?(n}=wVgtW^0;CY!N?3pFh!|ANuT3epr=-4sUB>h&c>BX zbJJa2EQjrO@nn>^JrvzFnh^Au_835N0B0p$G)nvUj|nJlNGlcVKC0Zmy3@pDfSkS& z^CaLST#OTHDLSR5$vs3$#xb2^OhwJv|XO$Q0N1aTBv3? zzrgaK<^yCU}JBgPRcIn5F0Goh1-H&*;D^!#^18UcOqL(~9XC)!UzTa{p zvUI$E-u;|;t`B;Yb22fIOp+RzUir&T;ooE@Kmz8e=##DO zLQeWT!Znvn8rU_m5tX} zj~sB3e+;Sx{&L2lk?1g2V|en3&MCApcl{v$4s4fW;4dxfcxCKp-KxP_-hQRf@BR+k);OjU7yKst&wBb3EA^E+;@Tzqd(`9|WEfr*eFxC%U3VS%fZ_ zR}(^{J=4Mt1N{D>ze8C$W4U%-WsgSSuG*{N1{foVyR>E9**?BIamhB}dO`uIO5}^m zSOcX=j88aT6ncsvh32zV3$o|O_DTzZziqAsjA+J6Dhh_$ogm%bSjS~FcI;57mGZ?= zSe@F<6%i6sghu?ydmr?w$l%wDXy4Q3@OFf&?>fH$vnQVX^J_gWxeb*o-8cw1HUI!U*X2FpEZWW+dN2q}DHoVMZLNPqd9y_9};4>Mtex;%2JzMhngV`C8(5#ErYMil+en7nl! zTLHUC>R3sk^Q^{AR@lvCR>nD&Y@dOhtq2m(E>e(#?^rY){XJyc<;&n=o!uA1S&pwn zrsn`Xg@e!%vRpmAISP*G^V(e9_>}2th`J;!dRXRhT30!BN!Wd#30)m+4ox6{Qf=~> zzseX>bNc*#yeWRaY~*A=!rz|*knO`jM1R>+Se})oWwZ#^5bhZujH5!EGv01?<2fKev;nfEM-QJ~+%I}YGo>1T$_gI~AH@0tAI*J-18BmzNd z;$K--*?G&0+t!~8T&;UfXr(aW2RAk8<29I<@}U$~;)Mc@wQKC&_?{ zX%-mrY^uGCK?Exo@Z>=Kb)J|}zj?S9NDHE9k`7Y*=%LcGIe^EqY^;@go@YuSg{)3y z5F8oZvDl33~<8=_+cy~S6 zwj;qHHxS7{xIo6>+Vlmb?A=)ci@U5p=-7Gz{rs=weud=%&q+mL0AYC7#3F0w=u7d61CjcDo4_ck{b-jbNLr1b^NvaKHNGNz1 z?T=30AQKNvgX}{dbo#2ADg_M%rj8Yu7KLLHI|VvS%WkOcXzq=rm>-P9^(uv3ndnNaltDL0MmpE;O^juF_s}ZvvUvM+vpi;(K?ZH*k;I}F zqh53FW!)$hf7Gy_0df8D-#z=wg4j0kmJ9Qjc>WTr_t2l*N%)PK6g!E`04i(UqILY?zUs$GhWgNPyz;9ffUaUo%mz=M^ z8X1V5Y>xQ7b?_s+Dg@LgDBI5R_KBF}9~gIBlH*QX7H+WXzOQ{$cVjY3jEDfy@?l0m z(&I(`Bjz=4eo3^*`sjh=2a%^8PApO&rtfVye(H>1cbzfSJBza|o-Js43Y8phEWVj@ z&4Hr<(^)|Q2!gAeB=^x}A%4aUt->;am9s%PVk=P7m%vQSIgimPM&cWElWewG;9R~q zoIzT&+-83Sy-kK5ZhDCOIOG@LcP7c_j$$4MswxI!(@a+e6QD|GH*>dpAu1RUz0h<9 z9x5#<<*^9ZJ#IG$O2}s46f#2;HoUC}Yy}_LBO;+pD5LPk$pxRyu~Q8A(6Pc|na>rH zk`vMNEa}B5&4K<=To2^--##oTXOe|(vV|18}^j9gv4Q1({vxZDjOE5cE0tNi_Y~;>-|2g{h$&v(iK_`A0a^3PeLE zN9e%=hLAO^P?7MTGnUpBpMrkK_rzjstUYw6E~Wc>puRUh2@Ylx4i|=U)a_2;2Mt;b2(=`yA4?fy?!B z@~h})3b?j7cV4!+@&fVSB2Vr=Ib{7p$rvVQPI1l0FfvAJ5aH%ZpJ$vV!U0jDY%KHo z_1x)`km0*KBWa6h>4;%)Jk&fiL4G`M?|(J~%!egx!cz(0XR%UgkvF{G@*i2$YtKla zE3Dj|lQ3QN7fdh3Jd zb6EQS3VP+8LK8x2$dYNrP+fthmOwdaeo+N<;;rep^@LLR9me^ZP1AZ7aZriTcX)O%iV2Xdm$P|w4bK@tu`rjxojKAl23i`MP*4ig{MhIvWnSx%r~^E z@dwZVEXq+#S&b2q(uAfDX#9u}j|WttzPT>#xWZYD)kOwA`*+|q0wCrX{(Z$?2u}msqzZ!rP-Fe{A_V+ecIli_+##K0|-rHTY z$tr=6jdZgjJcC`?MxWpR5=eH(c)qD*7rq z3}EAF$hn6aYw44sI+eFbgEsoV)|a{Hcur{dqJ|rD?0I>-+Zvr}K1~;wBJYUy(;mBN ztZdQhi}Y-1SGY5>q*Wi=|Ev9T|JwD>j{+!6OsW%+*$XDx7xlXYXSu^9_*-hd=r;=2eRNv)Gg9xSuKE8X)7HJ;D}YkA z){&v45JkT3!m!dSGeQ=$Ar>oQn<*8^{NZHR&s# z&4ceRnorh7!1alOpcUrf`lx*UGd7Z;VpkY!ePlnS2ANMUIblCA?esl3Nd@B;1{y`j zWja6NPG$rcwW6`w*Ahq5Y!2iFqw8NwzOX57QPl;uS_{ITH9dvceLi>x?qLXAVya15 zHHFTJcC@g_v>%L{?GO**>AH>^AYOaejlYb2WpX2}lqd|#Z@nw6BC;tu9v|ie$*+?r35n- z{ZFNpnrovTQKlr5%dsqSk|LQW<;YJAU46b+n~>f^r!bi(PSNcb>F3SGkiC~)Qq;FDppHB%F0a1RT&Yo8tTi^$y1JSu z&&!HV23}UAtYNJMK8+IPxZF)A-rF|z3{D&mX1Z%YNjc3bNE8grjDnKC)MD?XOPsxyb$tIW z(24@+Quj0@ISBmE4TbCEMdpt$?jEj36Vq?nXT8e(=x{TA%`|{n-(U4Y>LismHxd5u zW@g!%Hww`eR7rX2*gAUfL1rov~Z&xvp zBm%ktl`5h;aTFt5x6j4RXGiKM(iEdN zK&*KPmFVNbdIYY+iIvmFRotkp?tw=;4({PGyDE3|a3kFvyM4IAR~|F8}=(WzY<}UGXTo2j~i7`&YB_l3=-#2M(#CuyvgYj|hoI#>;vmYqy^IgiDPYwAbil84W z=!jy0)fIX<0b6A6?Wr`i5qD`K87t80g`v{FFgmw-m}(#ue$LGlfMA+WqJpNXuVQn8 zoz$AytMi1)-as&fedmc@Y^d^qfqKB1Ow}1}QQ|jK0#hLBeozE93Q2SBV%ru}HC3v# zoFLt%WkY1)1hP=L?;o*{OL=X;?BO8m&s;+tof^zK$Z-_mCImyM#D8y+gLy%h zX^{fK0zk}&(=`o(jOOKeFKKA@Ij1d!`)bV)n3Q?bm_U$04}ODo`PPE{pFeObZR*_Z z=#_gFU=6AOukMVt`kDBamAaaQWSmaq=@gf&7A>tEGsAW&Z;i+*1cggjP)Sy-?gyWB zijCq8o!%O!PVP$|6gAx$vmabMkb*)tJO?J+8>(3{V`WX)_w@@5oWE{%!Cv+rxGUEj zvNp7VFzAistK)hC>7QhM2{j*o)N&-HFC~a6F?-*rzcZCJ=0#-Hf+?3)mbv;rBgsz? zS6QNy#xPRX7DvmT=y4!tMTz{(lc$`&3OKB z_;41?UA^FOMMj-Bf9kdsg)K>^_ajs>cZXiu$!OlgIV0pRwb|s#(}kn3jEd+Uo#yfX zyJn#qPx%FKPa!&~?O3z*X(kT`HbTIqjGm8{zdG1mTIzL#DY7ZtuH5hN{!;n2W(w3d zsrh|~LsUoN#=zB|`D(6Ea57z9tKTe`GmOjeuJtRT)UMc;J3r;3QA-@O zh4c&KrNkSFaCzlR-!2Fb)eDuHa0wfME3$K@5A?SE_X#x*Zf2D8tL_VXq#jXR^*H2?%wCqPs6Z4MHO$fOd)zlj@L%7Qpf z9@bvLeD?G7)uG;B^8n_7hBRPB>JxFzlyWY;DC+M82fQ~9_9j~gvBdr)+yL<<$Zh6w zIF$5LvHXU&?qJMc7t3?v-|pfJYN6($SZC=0o7JkwNVwXS-;Djzm{F>xIAtFM`Hotm z)rNPYlBtwTL}1Fi;sOnV--oMYCo+oDX;yI@j~bA7HCho)Ey+mn#&_F39ZZ{eD>=M5 znKsEz<+!v;O?C>wiGcg)OVKlpDsEjGHdDGqM6`Fd6dlCN9dE&hz5+zId09ER4RlRp zbjl&>+V55`n$pieAycp2(0a;t(z6(xzh8MwDTkhAunsHksQARKL>%G&z&rAFRhz=S zeZS-P%0C4H7Lgl`0NKFm;AB!xJRuyW;}W#Lxq39eFn-12C>WvcHvAeuR;agt70ni# zF|c|1<{EvgRyh1$#XHS4QF%)@A3+Z5i@9zp9ir^A-$zl>AvC9m{Ow5K z@SFRGt6NwW|v_(IAQgrZ6MSF!=EYa2>&WQZ8oHp*^(Ey zzgmz2xGj>(fPX-MWp`{ul$K2#dx_gECd%QhDKDEa&I^K=z+}TtXHiT9paxGkm3iRc zB3+bDn2MrB@3DYnhHo28Ozk4VA2to!2Yj=7b4@kvTI9vEmffRfLvaJ>xSAey$RFvC z^Z4Na@_7b~4tdn#Uz5dg!1&fq8eA30H3-BK8EL!ncT1pnazUqSwm013L=rD%TO%c_ zC9_0#i9&ux{dvveqBge;G-s-O3yTe)`>RuzDG$sr#Tt4?$4rHE3apD&!yz?3tOg;^ zNe<6)SYKmCvii88^B7|?Kl$5sDotygC&w~A&SL8QtHQWR?!osr^AB#|w3SI6g-r&jH zR4)X*Kp}g8VV3q$)~`oth!_nYjH7HoZMRopw|Ib&<1S5Dzhp0TaE8Xi8aUF+JIq@U zPT@zi`0OL9$laj5E51GKBGX$WRpS8h^l;;<>_x_-AWSBt4*~h$OihM4)*@-HDCr7c zuPVoB^BtcWM^^nXvQsmTJHxUdvYJkud%uAgB&3`)T-V>Ot|60;1>CZ{4Q`SDXmK9Q zEDmg5ZA_uLb#Q2({Q(b^cf>=&wj*i}v!{#tn+X_b@OAs`a48lfWrLh;p&GWtx8~wy zr6;ozY7h;?;;+Ts&`u*CLmsfH@}vrT7~g#`Tkcmn9}87w8 zzKn>Js*WYCQNv{FRjc?B7lGrx);Thjv$V`t-L=nZo}~?`I|;`dqTD|z!vs(S_#3Nw zKbuNkAgy*U#0gE-*X5DEL`x&KG++DH3EzG@eoRK=9d1y_pZ*C$!`tmsBPsU7Kx()J z%Z;tSF*1XAS@&u3&|1Tb#urA=9eK905}$kmN?CNPt2>mz1KY8soU&ez@KSH0GX^@| zH_dNgM%5{Fj~5^rY>@eosKoXN+e~E(fDXVL0C!iIPJ*avUjU|(1R;cFUd0N_*T_kN zd>>~pOG@Q=rSbYcoCcglU0REVh@v(`5px9?UsaY^molM1%H>5DQ0KbBfN5bOTE*0A zk~KZ(R!I@=+pb;HD1@oQ!rrHkeL1TwKB_L4XFhp32(^H2rNI9yA)iw_ba5sfT6cj~ z`pU$<$lxyq6poGvpk7Ms?twKUm!ReHW6VrQ_v)VY3QN~OY&i$uVgV&Uo8BXmnrpl% zqcK$K=^-n%TlqxFuYfe7&KI3qBH=N&!>uIq~Dh(q^`v&14F2}^gwe^ z*e+MsT>kQ_Z#0&~u@K^Y>!SK%FiG}U$&LM~p(4;B?f+y>djP@P>&lbKI4!2DIs@7j z=~UpfWFqmx?8a`KUmS!k<7F@OFIp;kSfZLu)m{?M11b{!T=p*{O2t>eFqxo^_j*DY zM5F@`QHxry?rMN+w4(Ctz=gpsVw)Um3}3QEMvXBwzxpsTQ(tQ^RjrPmvcWAA_R~0o zSxPP{t+x4U#ET%a3Dm2i;b$6 z@xvXlT&tTid@HMFN}mS4H%xQtU&9rq z4)N~4>zIq?Onxm4lb82I34QAnhzjvOlv3)Zqo9hjdG^v3%>xZhC9+u^mlaqY95kK! zj6IxvMP_P%4NFk6CaNvM@e}Oz{qK|4Ss-b>0J#BcmUcg1MY)3HQg-kzieTxux`4qv zXRzyib@9GABn>*Q5OzPp{qdK74vyF5EBMq@9kfc6dollq=sTO)gn1iuAq6xOas)Zs zhB)G=$C^rChXonaJjleZ#cjaY=a8#QS-`IfqtqzO+Y~>ryMmxjlJ;o&`Ida7^6%9E z8w{5lBWWS49G?O{CrCu8yBMLF-&%!mag~~K((ximh4rY=6d;X2p-Uo-6#oTvA3Nx$ zHwT$A^JAApY=P{zaw;;AsuApNxw!4V_eiLq^S_aM7U`p%+XM9EF_$npgNuI&sq^^g zytQscKRF$a2JUaUfq!uTq;UqZnityU*~u6cE5*Ixpen5hXxzU7hf(Qrp*VJ;o?M8>=fEG$pF zVJD%7xB*9b(6B}2{HfU>)ra(iV-@Fu&ECoQmes0;dklCIAaDz0_Ki;*p>PLpCt>PN z`TEc75lNgOWPW@q9D4z5p|nNruDs^=N%+5Wk@~v_d*Sl=yxH3nh8lnsx-uvZ2O!(& z&w_UOk&>?NXLoP+Re3emTgw4%L2IbC$;|OqEB!3wa>0msEA<4jnqJ44#4jt4g|se% zh$(cCYrC27%)?*P*}ALkQA{>7(~yPEdIei8B$6b4&V;3mvUGE9;OSI;pw6VN+`M{` z7cEHDp&HZj&5<)OVi4$@0_YSCl|E597(~Djzq#rG)D$Ihi3OmmVKMe|8Wh_Zz)ps+T1Fj8qJod-jwba$gFFxA7eisQ&&V*BqU(|4mBkaXqdYFV0G#_zJr;o z8AjY(De@3^ZD^&ramO=Vslq&f{PisyPqh6^t9v5dxCWfM^~u+xRUc>hfRf= zm!WF-ldTRGwnA0mZsAxrQq?haWNPQdjUHXJbj-#k&>4n~3-U)H8w_rqr(HfD`-(i_ zbXjGuvfIL6HP3Fz=2jR6LUbiw?*nRm!w#u3L?X5ocDk=h5VBYKOT$o?DUxN>eT)$bdfbgCj1Y}R zt*GA=y=oSp28$M+=^Z1?XZOFo!@k5995Ks3fL6N;#+6`?O|^O@fKp^BY0?_9Lz`}} zt#ue1B9krV`K$Zy}9Oox@Z!=_u-|d7(;IFQu z1V6yUV+2q4x4D`7hMGaxT2=8X36a^bx-dZ2m`?ulhI&xZg44xHVmw<$VL$H-|L>3H z(@HEtf4<(8<9WGaE8o{rgS}7H5uc?hR$?x*PKTzQ!sKmHD4ZHoO|^`?Fl{>-4`&PF`}bOK>M`T7G5v^9re z5a34RWaqE(L%`^!t`p-W1q_J`)xIu@v}M=L7@1;I+KEj2Gwd;_EwN&x-N%SSCY#om z7n&Ii^{4--Z=QptcDuN>89_O;8R%do?mydu=)E9oQQkkgT`;a}%SJ&W`_>cW^413; zayoQ#nKi`kfa-xQO)aI(>_S6VYl2`c&*R;ef@lYZ39p5%#Xc2(LPpXGW%&@UQ^SP# zZfufznGECVmi8FNZnbnilUT*B#$Yw_6 z+sqWg7xW43G8rX-;Xn~wLE@vFtA#H*LJ6=aivLBkG?8-`Pcg!O%z<~(hDyQW%9Is@ z);vh4A7mvL5~C|K=j>-K?LLzydf0Y}3eM*AB)+M$mI+hGDwXB=G+dsX8bBy0(lzs$ zRm%BoP!2v|%%`W^vxkUp;9Rte;K)|e!98Eo5VS}l!irVTl|`g@-F{Pvmb3=L^_ABT zyzEAPcWxzBgeKxy`V2F%CZyB1t}96NSmF|UL(y7~K#SwPf!$KZI$#@K$tBNT^uqyl zqtrP?S|{l>JVP%Mq26-GsND~1B~j!rRuSKpT)Ub)cXUegk zvefyUFQWKz;Ny>T&6owWo1^G2(EKCfW=5ULv9&;X@WMOLZKdC}a%OM~b9^O7YKCeJCMTEfGjEg2*@bi~;ZKSQPWE9)Z$M(DRuX z;dnVGvOYwjW0l3*_7Ir}mg9R_;^3+u^fJisRJEKYf2xV&TGd ztxO{!=LL!g%>#Mbn3GBq%h;k%4*2A3Bqd4(f6I;!4JCOThZ0|j(yrC~K6wq@&s;{b zXMZ7<3N~&8@EVf;eyGzVX!VeqBw=T4ThWTT}snd&LrSiH@)QN%LhL0X+?R{l(_<-T|(4?U%weranol?E+s>@#V z@@ysjBnQQtI4gkv#rHoAi;vpj@2_tH_%wYil?IY$4Z9WXzFB@RoQ8h805rX#VN}Sn z(~&KC^D%#ABgS#9C{8GR61)aIk0wAVA-1J0+@CfCvdV{yfjS-JOVsa@lY_3hdr%a3 zC_-zFfjfZN~?y>LCIkmL}jLZ(zno4bOr|WM8cPd;AP!h-5+L(QYI4(H9BI_?Ogo z&FPf3mOI9GDRC@K;t`(9#mN!-m+_W7{%RDYlatSK6mNkr$%+lT+%*WMy;z4}_!ur6 zNhMPYChawl=Ybbno{ddH>P3`qS{*ZJwvTPLqjii{^_?GPis!0!D64q&wi})#f&AW6YyKe-M++sIcsu}NL3ao1=pCl?7~Uan!0OLn zh<~H2lE9vmb6D}U%7*vX-R)#G5`j0XR&>4oa(OqSVqjImwRE?1KZ5<_u%y(B!j4MD z{zDX}H^G&Wi8L(q*EOV_%GQaA%#Chap3ALm>P=kA{ccNjC`jIrIeZ$ zfNCy2wfvsxo3@@%Z8V^8iXyY@_=Coo@$p;bANCM)`s^2DDfYVWcA`{e^|`~q@;>B2 z04{foi`OH&zPP4qOf|e{Z!WlK#XWzHVwRNbOWW;I!B98|*}kgk&Cz zP+W2@5~B2do6r_-n=-4HcvY=^8-+@B$Nj(zAo zM9-<^3OnEEoF(HfMPtfKe8F~Mwe45VR3k|DEl|<*4THQP3M=y}hyYBck-#aa+kj;;B5IeCA_Mt9! z6-$grrnlY30*T#+6j=kd+^V6*JXC8~3&Rj1fjI>w`W-W%?Op9wp&=^t0`i^vQsvZ& zJ_gKrv+vKq1Vyp6*CzeH-{ay%lCw zq`$Br6<|~$g}(;-lutjrI2Qvh197H~*+GZUm!S89DE?ulSTR~a&lQ?glT0taf^&sx z7OD--CA;cWpS_=d_KL1rHSY)dTFaDCOQ6z|niBqw8}QZC1WA@s5$k!aewf7#!^RLt z236e%^9G#+%dP;&#Bcikt<<^{J%~pFXsxq>qxyesMz#k9(h+U{sp~M%vf2r-V zZq{5rDuS+odirR;yk|_tg@o1GBf9Y_i`IJlc^}T2jN^ zmQzHgpGp9qXi|$?+*KSL3FD}>AOokd0>;&a%d1Q`oV$UhT|!R z`9}=;W*~~B*O@>`KSaOH9&Ew;3$J8TDG(}Js*dqj!<&(?z2Sk{eY8j2Sh6L*!Bop= z@*W@E;4AJYNgv4n#aA%mp3h9_#2C)5VSHMDB9@9s-Cepc_9~_#p_U*BHs?qinwrlz z|Lu@N(BCa1&o6~hcOiQw*({QuT4LPu#jMEgU0xlq1)2-Xq34KXirNEg5Mjui7Ah3t zpSJrIs2+Xae}w6ilt{I#a3t;yjJace;*dzS#PqzFnG5~6TJ<3~r!rfwCAtV!jChiy zgS`=bH6@HrQlvDxK`qWB{+l&RT-yxBYf?B&oEd#3Y(XtJj(OC_uF@Gr%M8bka*sbe zXKua+VY~2}5kHMFpboAclxxU7&#YK()q6hJwHAyhn4m0!cf=U1=7Z+u6tH)&9UGR{ z6;K6UFMLhfHKjzL zgx;Gi>OSb%-j$!r3TuaML2f+OW*)W`x!$lU$p2H4uLR{J-EpWNc7CWJjITz_b8S4hXG7TuIq z;YM~km;OGh4~WK)1mIo)0-0r<26qgu3LqRBqFsMvBG4d=5IfO#tKs2Xc%xmi<|-BB zPgd-yH<sPKjBEj zcS6SzyLs7e-kZU>TWVqx(yL-WG6kF43SfL@%L*igM@h3XTJGmzO0B#~eqwm??spD&qLO$%dJh-Q0eunC4^1(( zQM*wAn~ey+Fk_tws(2|H)zeiiSK|Mbg*WWp}RZ~;SSC$`M14_;!Z zalAR?4C#TocYc3*Ln4rA#HV1~pd0kT=iJBDt(l+Rji*^Qc9iX#IMN3h-ursI)DNE6J#cM^SALh)x!)`VdL+D8bqHUPt+z^Y-qIn zlVoF)AqiEh9Yuj=rL^b6?Wghjq4}5BbjBz7N^9>vFXA}b6vbDpV;pa&lKuzlkUIjL zhIS3^B7YAmA@dD^0#op>+Xo{fjXt-_qW}b&*J9Od#Y3%=B2ePXN8|xaH&*T_=uWrH zE`fb>Tu0&>?)pxGto{InG8yk*>F36oq{2vNI%Q2&xV@lb zR$a6ytAR74!(YL-iFa931{$*qJzUX#3zCa*+>J!|{D7CBelMoCrDJsQz$L6;5kXtQ1o|%R2As#n+gOE+wA|JKzlUHg$P(a z{28F9WN2ngS>2upxiDEW6CaUUj{EiJ2x25icBT6R+WmvwxW91XP#3!d zWqKEd=pG&`Qty(m7^ePq>g7nM9j-W!yz5vVR7QIAvp&DDJ2mO!nld#B(~ zz@UhAdTXV`Y(^xgcnlv)oOL8pX+f^Y%ms&D{BHjPr~xypuOxH2nz)Yq$T;nv%yAQm zLTkYUU9pLu?z>7S3F*YAehB(64#Ams8 zv@cdDkh@+jgXSo2i%yfxtFA$hyT|PH2+b7n>vnqCuHadiXpk&wmZMtdtbMbBd@}f? zh*UYl!NaY8Y*c?98ue)3+raU{iBsOh<;cTfIm2#8S~9%V-Es#EEygv7@VOtL8#cQ zl24=`0$Ed#X7tOq}RkbpZ^O~mkuhFyZ6Jq8TrWH4Y#)rXmv=xbf` z4%x9uGE^4+M^Ip*XRrUd=yVPvm+;q0)lps%Vj7~8&XqKTJ!YWyvWcBGOg)8jRYL$v z<&naxogQz>t`rRFa;Vjb&0gXIwFBs_TFZv4=EUM0Xuh4k8@8?8+H{jV@^EKl6|KC> z%2tuyNURPC+}cv`c8vNa#fH!6?ru6dY786uErz-+WD0Y*@Isu;&VIF3KxH<=|KNL4 z(_y`#XZ~|t?9jq{V!(ryqj3R|ZP03o^3=e%XKtEuZAh-l_G0Y#H|$GTWat*P#s_R} zd~v*?riPQy(R^g)VB@lGMIk2Y4}&jd&~L1;oEwL7j^K9no7V7PY#ngRFQ?wKli6Y# z`$^m)gm?1=H`+K0!8qa|Z(d3}Zi(Roq~RJGRn;-Z{(R_zk2yA=e5apnY>}T z@k&bJYU6>0cl_=lSh`A4O8{k1b$?m z<~`1>^bOyR5Y|4{`Mx5lupr_ON^Y6Fs?8}GOsF%Jm7f2D)RZe2`R!lk87I!eq5@Vq z7A^SyZ1T_^_?3j0Oz2rV8g~rlG8U$_3tME3?CMzga@E@L@G=*t4d@>;hl;LR4|`1)~X{A zA50>A?-dOWkdD-^<@G>vC5q@tI2Baxynw-Ju42p^+oV$ zhNQneG^ps7g^yQGUlxxcC zB*9VZ_g;wkj6k?Ef)$b{zxn!kT@6=c7Y+)yKx|Mt6L&@X0ruzBTwlJSa8tRpm8XMb zDamH7APvmVi`XV911X(g{sq-2fx z>URxg384XHYqlu*HN4{zXlfaGhEd{jr+8St ztPK^zIo|*)K-9m{dZs{lBE|jd?9wYl4?BW~{+dg9iRE#Kcs)C!zyhEj^SbUGLwLJk z-AEd(q3Mi1@$08doi&(k0#JdOmK&g zvy6st3=kpSi9qG(azg+O8R{6!PtA@2|y^$F{vOflH9(?)EZ*9M-H-1MdoQ$8k3U0XsmK}noApXwa`qi`CX{T z%m>*Irf&YGf(-4_^I|sKAJgLd&pY4l02G9^{BV8u18VKz z-ZlmUPY$e3>{y^DZVpE5p(#e)6>>lf8i48@1pVPTw=F|Ktck0$^Q$jHsY|_=ZEa=D z^@P8a-4iif4)pWFJvkH|Nx*rjpf?&ZtInGIe7lhE2Z0%hO&Db7J)|N!lu5Tvyh|4n;KH?p&I3PH>Zi$` z9aX~8;!R*5N-x6b1>#3VqJQy1|8*f1C^4=b7}{?3o?&9icy5n z7M@X**pHZYE)6g-tXAg`Kr{&i|&k(k{?V0xCvm!{RCcTi5p8V)~-nX z5T^52J7u;;Cb{_zRP9;77|jvTIMiu9ELT=f30TqJs~>3&u)Ke|A!m+wX#Wq_i$q;l zlP&+Y*c;}46g*F#trQ3#B5v)123I#o`KDSgGhaA1yUY3_QhJXw;y+1*v`^cL`s}@X zP$(qJ#XGI(lZW4K`spv*)v~M?$r@k!=HsksYBj^T}r@Us$Bi;JGn_#qAOKq!lpaH=BELIZ+nP_9eO6IXx8_u zI`r$5-85@M{%*?prhm>jHGZ+Dnk#Y%vn4%$ePxg24?qpSF0e*e)7A=HQYO|x{>!Wn7GL3iXwHDITD5)qtag+n~9ix^Zz9W#z_^W9%SI3&#Aamt-m91w{#neiAd^$SkTl z1?FQ7hxo`jFfOVOWtiwWuwTVEm(13(y&M!$NB>;a+$KPYzCB(iVk2YuTM z-E`~3?t;j%IhJ|R36ET2=fO-S!Ho+WLen5eshq=6w~|=PS<{ZNP9uaa2g`H;w?P8H zskB%SZISwGMgQ8aUp^quJg@nO&j}r*NO0F>+5Iemzxmo6Ju-5W6{0QN6~^-L6D+0e zq%fgQ8QJrn)$LNL)2rqoGnw#SG^DhY8O-RsxJchn&U5`lbn1Tr=majk`P`299}x)F zUWeXaKfja6>6PeWx_SDvu1XGQ6x~?P?NQc=Gc-0EEl?DPD;bf?m2wtT8G_hUg+1I> z=x*|=OYLtt_k)&vrGp-m@N`h?%FmumiUmn9bv88<*`#5S8R3A^A0k=NW z=TL{n{&s8)xs;e~o$qhwnz!5%j+n3QZ5j= zmf1L`%GAlG%n6a}KY_z-gs5DNT4%qG%!xBg@ydwuF9uxUoK%(k$o31+&%;+=(Bn#a z|9vM2o46?_^-3y*GX%SeKPqFNj)?<8V z0A+jF-0apVv#duA;zO|_CtQpo_mb+KZj9r92L_PvfaPZK9N2+5NDK4V$-6G+q8$iz z*3QlQY2?YgHjhQBj{XXegw1_WIWjH=;Ru<&ZJJ5TYCmc)1=U`4Z5#4We>sQMVVoL?wfgg~5h_3r z8=aDBjJ{;V)#U-rm+yRbCWz45@3Q;{HnR(1}PnoP}a zaS`P!OXNhP9VIb{2MQ2lHT+jqQ=|Y|Gu( z!aIO9<^hYl9psMrFrCK|AIM7?KR0AHhmAn~#XW_eaqiZ_2;jbLHE4bc@{vB|C{Ymu z$rvk~zNzE$3U>%UJ5ZV-w{H-VjnN9SQ^nfxD~(@V2FqE;S2q;InwhWxIt(~Ewt^rP(SlkXG$v)wCM#KEQL zP=f4B;&Od-^$e_GGI`K82!-@gI-Arl!`cp8-ZlS5eAFE6)utx;uF;*GN_CpQ{+1uS zXUx{Z7m#crI*{D3O0Hkrsk!vCYx`D-mM-_r09`7=_})A4zrvt-^n~IF%LlM`dBnK$ z)uX=g2Z0-u6|57mjc(p0lP!d8WjU`m_!o-Xg<3Z!JQy$#dZ+xE+vr>M*NxyBf)|PK z7bc5?jwE0sqq1-f(c^r2H%JoW_G0LQt1v;PG;O4f5|(>{6nuD=M4b=GV29P3q@<{h zplAlC0!$@6=y6E(Vt$z+_FDf&Oiz+}aMC)~TE z`u(Zmp7miUum(gG8nK z39yMVm}J}|l)52rLT+QMSQQPVL0n`1(Jm;^C(nA;FqT4B!YA%S)&emP_+*sdbDq8cOwrlw>RQ6p9c`bN%nIdGx+3slT)j zC*3ok+e6Ap7H`H5G#4EjS?^T4wbTZlHqYRF6W_Vy*r8VJ9j_MJS?)FtJ9s+Mf4pt` zje(yuhXEWEP3R)=mRRZNahBFHl^bcZ5-V5>ORv2=0=UVqL{Edv=cH=a4te3rGB?EZ zLZ$@HKtPfc$HDPb-r7=C3MIKO{^b{#t$ac-Al>P^zptd5cW- zp$MW!{$@*;O?K|iBO^@&{ze5AmKU|P(Gy_@_?zsv<#^0a=RBfz1$noBpfU8EwAQnS zUzO?s!(R8O!tFZ*0AIj`Juu^xS$lskhwMh9)5c`z>Y$Hq-rg{X;fHn^Bd$82Kg{TB zbs+S9NX^FQ7+I3$B{zlPQiA3g$RzI8sblekkMu`n1^Z&0KDeA+CYQC*5%dAk++&L~ z*a95FB_KezL5fPlPPJh?L~WV01C_(QPUJ0+&s%_6Y{YVbM;GmK0Avq*DF7-aYf_yU z>(kyVa71sgI2gIoR!@(MOHKcmwk)LN6Fkfaa90msV|7{OrIV`{&szu zI%(UV*95lpAv>!iR^UA~Qm|$l;~8aJbUg|Zf(2kMuXZI+%sN1`jvaDtfCSX0Vs!47 zW4l2gnFFg=pfXA`EM=-_#)%m7;Ilmz+pPlmVt2sT%x#t6+V5WRE+bCWv`}x-J>6e% zd~Ln1QHTkLU##|Qg0&20YleA1`WZAIA=DmpWWOFI*krA}5H$jILINEr)>pj8UqFIlRNuRq5y(Af0`GTBL-Jj zm(}p=3>tUoSe66nePZ<8;HWWT2wOHoDP$g~q7KPJny>0K$@PRj0))1n!bVLHKw-ZV zcN^=URY8qn#(78Z2TX%eIaM%u&ItepEcMvCpH673x+iHL-vCT>hSeg30D^)GzAGwwhd-hG_03$m%$cJE{IslN&t0f9QR!1fg;i}*?<~aZHS#BJ{2>dR zchkb*2V@;$-wy2rvnT@Uq4VEX3pL%ap}Ws$`{}1Uvd25WQ^J#&ypUlP{f&srUy`L~ zz~t6?ml4Jrlb7s7EjGP8H9@xuZ^4V&+oy87WGu03G!53oFenK|&5&E=Hs6LXDWcMk z3+o%@@_2wh+l|%* z83a>Mly>o=Y{f{a=FxzzFNT_8)+nO9J$6L%v|jy{xD6BD3(=MRPf?G(@t6D-%qdNb z=6UWS{wnAHVLoYEUO?c-Q>bBskR%rbgJ)B-TNPpLiU z=5o)wpf>+>HDB$aCe2V*o~c*w$P_>$)mD zE&n#5?)O879Q%~PzMSfdJ|rgUp&r?_x3Pjeqs>fPYmEMO@=MMnTXMH# z>xxGWh`1-<{K4I}$JQshQ-M`}!B0q3{r<;nXA2m!P&w)Ct?Rj|i;TN%T6-!9jSzwt z{X!|PqQabah=v{zGU(EAJARCw#2O7P1F(Ek+rgS{S>{7BkPPBV+=vr1gJ3VVPSlka z^hyhNcCuyj?uP*3K2wNM$i4t(bn$g_WV_mUM0oy83O?-}W+6wr9p=3(G2673O&>$U zfJ<2nXHgK7)82!zsjAFyU)GTxg$@`_8=T@{>JRh^P^`)+b&%3QrueuVW;NZv-&CZ`G0*mu{DMKvw@`2{Y)sPvk8or?qV zL`^0f^v=>LR}_(LqA9+iltXTziZma-j$jU8qm!2!jqF%U-o(^2Z3s<(b)aK%eC`_F zDF>x~+~n45a5uDs5lNvyq5_ffXn0I8B=><2otw@7co^@feNbMHa9iqMRIgXR3>jhg z9Y$J)0rRn2OH2^0k-_H6NzcRmfKOAoZ^XN zjvF6?1^pm+jodm_7wIjXxhBg5w9atn7Chy_@+i!N-r`J1j)`q1s?CRWlFTx`uK-)2 zXH4C^f|B3?WxVQI@N`e>VO*M-RE`>J15dS? z#KIrn1`m+a&=_UF-d-;fOI!ggpDT_d9iZQLtvr4Z zabI33Y9e$joA@7C+XzjGD-KZbV1G?N7hP!5tuZ}s|Knk5%f$juwBlOY-#~a1C~)M` zfY#M6R5}5pVfQ)Z6(%|{lk0ex&L`P>PaEbcKihV9qp-C2q0ly8f?mqI2tP~W7hc={ z6Ai#+8~K=cfYizjOeANyjx9=~9h`HMitu^`lO2S^swjXdy9y#!wIQ=^3nhk(mmP6p z^UTBWfv>&nTHO!LIE#1AYFjoHsCLbK{4J6fBA4-dhY|*|qjN9t0~l!3YZL_lPbz1g=9Sy9a1xEkaG*Yb20pr)`(L~G2iL)QXpFqFE4TBddCy{3?^pchw=vK< zUmO#^^&{hKQ)}0Qo~FfN_D=)$1r%~nn(^J*op{G-mh~&IJXyv7re#m)S*eux>_e%k zTo7t08HCTSBkc}+ZVzyDKnhN-39UsKEe%J7DjO{&KVsCuq@)htP~Fg4W-!n| zaIsh%Z+msxKiv?9uD5m(08FT}>mxpK({?#RCGro%KH$~FYWkoJcr-TT&h_B?oRN%b zDp1rQs770}YJ0z*2@RmQ8{vY|ej2Fq+MaMkGWgsk7usQs`I+D6ilA&1xJlnXZpBpY zKCM2il>@1vM*8R4WygW@Z$zk)+a_v$+BYaTHi<0rIyWc8y?<8*wD)zDgf8>e(1Y5* zf){u>X2T^Eb+(O90(@C$&miK&~^r;mG!xRk=4ziWgv%$C6;&)gq@C9-) z^HSMoyuD=r1`P@RFM3?2uco6I8!x!f&-)ZEROl%%4dCU2Vv9cGc@BY5mokCvPwGfp z=igRVadl3<95@CsMF6p}H(XF25oq&@;kK7wN!Wpnv4hi)lzcapO+aOlwz5;s#FYU6e&Z-b*S8mITH0pvg2)&3r!BW>e;iIW zCWET><43tasn;}g#26e&5cN+yrJZjFyEZOVB4+gIVpa6=)?4{Xv`!||F@Y)GRL}(q zuft+4=;^+3wkWO61%Xc7gU99pNOlI#h&k|-k$?wyjJ)gHe zm)!906eHAqbyfTNttDI!Ei6pDTfYfRMc1bu8-WJnMtkK3Iq-{$o!6<|E-#2acHj>f z&>VInHJSh@5J0&6Nzm}MeIb}ah`&r{7+<-}DeKG+$#U)&9)QMNXv!1<;MK3Sxs_1-dZ+Q*XC`*k4#)$rK6KYRE+ z?Ex3sP>$8~ut{mxOVAJ(_;EsIzQ#viCc3y*>ve<12(0(C@#^3@0`P)r7qA7WzRfR= zzY)>Ld{sWM9bz{aVBIch`b1T{{RgtRla+T!i92ZiLuR?GPeFjMK5@Z@q3@OC#dP9bSEd0vww81~v*hc(_UK=`sIO_7ZHh zZxDTJozx0^5$!x&GjDuU>LDv0OgsE>zV(5~obYHet%NCZd<$$7D2! z{6d(-jkzJ4%qc=kNlTG9&h0YxAL&}ZYK+e${D9iAd*i}qZo{wo7z|yqDJKMhCs;=GOpQu@kuS5!-ud zwV24UXmm<0mq~$c{Eal|QuHytx>7J)qG8tU&PAl0r?mmqix)HUqz^$TuQh zzyU{ZxVQ>C8Dh~Y7?m&_F+W4oS&vE7Z*^pr>{?bFO=L4;>8vdIAgq_lM#RTVF}Bjk zy*2Q_2Vg<)mWa)gQHwz#q=eLQ;;WZ!1no>)CbDc4)n4D*{MvKNvuq1Fj%sl2r0@Lh zp;IiEioOK{uYJ!+AfS#lrhAW#5!o46_Ed=-$^zDTDkREL zv*8YaduklT*v?#;ge$-A(rHd%dC~7hLqXqSgjTh-j}D%3wwz5lVqqWy^~*ip>|qtG z&pm-V@=32F?nz-26pwp1z`^eyk$Zb3#xISB7 z59v#4&2z3@osjmwB8YUBGOwai=*4XjKR}zlBenN<0v$G2Rdl%SyeK<^2ti z<3M!{+?iy7og91y4o5}mdXo?v?;pATwhTg1CO+XBXj8ochA#3UMW`rmngsM4-V9?Z zpaOv=Gj%*g3Rj7I0tB(sI<%%%4NtvC8o!jds1ZBk!ugqkhH|ToCFA!$E&jDLO&5aa z=48yV)3mwOUiu1{3xKOnf~?ExyE3_mj zR>YizV`H(Oip{b{3~;HC{r23+G&Hzby+~g;ukd|_K~@WRZtfzA3+qm7O{06F>p-Wi zHb2??X)uYtj6Gl9{P<7eFjV`LT-BIC2_44rO=sJ~q0}rcLdNhN{eo-;{s5MNi!S!k z4^2GU^+jy#G9z%Uf37xDS=yzP<;0^Z4y!zbqkSSTgEy#DGR903j6Qp9L`iJw!(ViL z*3^vMQzYq+xDp<_uiF{UD1w}WW19d&+!Vx8>g?wYC+T_GeAf6dKy0fC*ANAL1u`D0S&u0nAQ*?u{-Z zk^f#2TKyr@c}?kIP9tG5;e7zp zZO^RM*Bg+zoU6kk5G$7H)`FZbzzf84)<3(-?!Dg$JAjYUUbID|i(r3Xo-^!l7zvU0 zo{tv^uW&oMC^3#TY2uq~LGht--IK;%hGJ~&OcK0&8O=3dQds&Qg|6igOu-7v^x_g4 zid1n{%-8HjcaH?AoBnxHjR$af8d4t0psn^Rgn&@VqUw4@n48AY#>-okm^=2Ud@5ZC zX;q2q9&lcvu(vZ3+gL2Hs_Sq94c<|w32T&+U`v|1{k&F`wWg5Y951n4ej@ovC146K zAZBjM-EqHKMLNw|UjFa>=X>xfhT2qSs_Ng>_sfv@E;ng!gvW~n^f48N6e&PkQPZeQ zO=;)M4ejWESxBZfwbXQH|I>ANdH zQoQa-vM`Hohw8!8d+5i}*>RHN;J`+9uv6Yp-(bs{F#xY?^DVWk^EHAnMD866zv%%7r*Kt z9)&f>y#hRb8qdYVbf{hm-dZUK;yS8N8_F*-$C{z~@|$UGl;pVaaV14gN(<%yNjA^j zP%M|3w5TZ`fgZYl)1DFuL>Uf_PzYPVJdm7%XtFKEzG<~~#V?%4SYF430Tb=>+{}Ip z6vA~p1ao<#imh_1XSRkOIp)4pb(!wK8+M1R+`L;sqt5glbznq%Lss{~Ws;oSp?%*+ zI$J7J8~X_43Y@hQDiDeuyq^DA#|t)P(M?D0bFvgobLi8}aFTr2y19hWa=9`^6M$bV zJvNjmI?fOPcBk|sBHtG=+Bmq0BpDgYQhK76HXfP%LoRFfgfWK284g&&h(GJ=tQ(;x z>%WShQu>W?l!LV_S^+)ft^@Th*fc{Hm+NoodGv*0_hNyGlc`nRFhRs7<*)3ow7t#T zfMZ6*eKUKg10p2Op$bfs0BVdMuif`m4X=;ri7`6jOt&?sIur>nX@?8F`dZb9VZ0dt zJtL}&!^dC%au1F%0X?C>f9^;?=ttC{$$l0AVMfi}Zo#{p=t~Cf$fNcasov_UpA3mF zfL!o39jUQ9ET03pF}$I{)~kZdMl<;Nc>*K*vnp@ud43@*d;c;=#HcG9T`;#|Aq9>>wJ0q*2z> zN^2jnJ9UHpZhL!;xWL-C+UgD^wgI7?3v4hHyATfNbBiOos%yN&He_%>1R2E3Su}eK z^hkxK>_LVv-Np?YsGtSRQF2ll!|in3m}`V%j;;+{1owBrvmWJG(JP5)wyg^hi7;$q zMg4$3yXX8(WMf`aEj#6iFg3)a2j}tie8ZUz?tH`nUY!`h-cmR1{EIHH0A$3v|HEhl&bYuiHQjO@q0hsJg#6s1MqSTb za`$6Dd=-DyZ<8o|){9Y!Lqt|ZIFX~Mw{$!-4o0`z#XR&Vqx4~E(WN1&#8CV_`?kUHUwlpKRy zIwsmY5Z+BBuv=Vzo?@H>HjbsS_*cg+8+r6>7W>7}7ak=EFEwv+hDt-Gi$Sxw9=GbwJLl{FIh6A?%XlG%uxh^a{Bww_*7B(M zHH)EO4B`C2N9P@q8=DtCHp_#-9lcySX8JP2ih?}oFvazFzVU+eiXI|jkJoIZ&!H{O zf?+eKk2?Chkevacr=I$UZFkZX1)Vdvwx1n0Dyx^OSuQm)P@vtXUc_BFx32+9o?g?Q zn`=WogV00F5jT>;#sN{oHyGK-`6Vz#s?ej51dmy|*}~~i4|_kvY%((8@RHP&F)pOR z?Ci91K?*hU+&cjPOpC*}Lu;*CNW@&evv{{HcXRMVrr_94C(T`z^sWKaGkc_LKM_O? z?xUYtV+N^tt#;pH#Hu2tyd-GiVm$}^7d{i)l)M7OO2}AZ_}W^I7LCW@kym3>u0q?3=O{j_CkY(&~3IZ=$bd`>9TVz|O^E6oc+BBD@X1Tnh za|}F`45V6f3$QB&O1;Q4<=_CQo^0}=FUK|i4FrHh@Vk4u2y!lByc>qWn??dbo}R71 z@;k8Fb_G7BpI)DyZ~0uD?srW(j5}VzgS=g-kve@Rk-|3oWxde)>s++4R@>=Y@0502 z3=!mW{x71E)|eo~eQxT2$jjYzcAp3#JGVJeZvQtXg%%R|>gw3GXl2TZM(3fosAoRd zlA#Kt>DZL>rf_^?fcJ9m_Fq(taAfW=@c`!-IX~)9Q;u_mXZz3Zf7QJ7MY_O}zYf3M zN5VTS!`#sAZad_Z4r;``|LKiqOD*6EBS)(6!DDi;ed=Rk8=3En|i@%cx&2MpQaX<9M; zS(RyjRU0TIwIP|gO7J&~fHNYC;tjRYjfGT9T7zK3yWnQ?Ht!0A0kT92e|t85(3reU zU+P8GcE&BQde*Kz z2@3&UghfSB)qrvGLgYYp%##^K*V!siDXL8W&;Ze3$Bp|PA@{dYf?~a)_2E%itLKq% zp`xMIWQyDH%j3sjYKwfEMd)yJ1E#duzHVTG8VJ7&dWkP@78NMZCF)+N>Kfx8Kd zdIju%gEr1Ek9Ak7!?2DTgaMJ?Yo*2_m)Q-o7bsoFhb-hqM}I^=^+~vvQ9Fe%aUb0V zZ&Wyys>DxQP8G?xJtC)gN{rO$K^c=wn+?dpL*-fx&RRE}8StgIt(U@6u%vLN0g(??_<1d|7f zwXxgHi1wA^0+VjAcFJG>fQk9Fta`|tCr{?Edw);*cJfojj=PnP^S6Rjzw8SCE1vqr%+r_EN2RcX{p+CVk(~6GkL;$jDE7$YcwWY~&g3@yz zh7+OJuY0`FK2@FUofn7J**qc9xRr!-yLn{9uf5yGb2BnwJ{X?9p@?@_%+`IOi(@95 zV&{L0Qj(D7?KZ1UGV6wvo|iTe27O8H#sMt!S-oqm!6J=KSFBvg{UNH<8;3<}cpx+}uEYMNHbMJTPh ztvW%YQ|TX=kKZDWXHCYy^*pkhZVEqWkl?>Oc5~7YgX}gJZ~*zxdCdh)BRxH*P|hx= z2Jc58sTLnBxS<@0_wM3k$IPUmb^kX@vcRo;_J%SM73gwC;?=~A@y3yRo$G!ame2Gi z*!r%3ATAEMk`?^bO)Xw=%1nWAHHldo*4WP^ArU{;?T#wS03XY*bzC&z9sqgWf1W+H2{p%>KdBrg81YfHdW|H&j`IvfMOiAj{SJ zo|ErBJ@BM|)AQ-0a?L4*QAbmsxpS$UlnOFIWfR`%fJ7Ag*|*{2PP=Qn7eaMYrm|y6 z#aga;`MN2HQj=^O0_rm3|5Gr}v`Rt$`kx8OFt`6-7X&UinfS9|0kn64FeiLhlG~PT zt_{<+i5r0buq`(;p}P25%z8#on-=_$Zq_mT2W0-XT`6utlDS5hW6Tr&rO34VSeR-7 z;Om6*q%Z57$Rh44s&iQw*8e7hTKLv=>XQiL$4%$KXpP`^%FT!$^;0d@zN%jTh5^@6 z*lpDRBnoJm_V`e1ds|@11`=Vw^(3DcBTnr4i#_PI*nCFIt%!Txc59xSrcU7I;pBO! z6)$gnze$`scq$~-X7wbYe9nGZJvl5Qp*TF{E=;u5f94*%sK%S;E9m?Cv4t!$(!&*j z@({$^Dl$CuIJ&Ul;r18OzYUHcMAA)!byOM8=}~M9J#f#7znFkUA%8eh8~D|X-ReB? zgWvzHTBDx$lk|$T`uyQ9GgXY31lh0(f2{=;GACpTQe@fMuJgn`db(k;SDN}l2ry$o z@kK$%AsZx*dCN}7?A1*rH{n*U=Ag%W2Rp|W z0&a~zD%d}+=D7#lMBquQ+m);htuFxDp?RF>7QAk=Iky!tTd!%wxp(IC zBJziFF%LR~s7Ur*+oE_{@!Vj@pqd-qwc^U*oLUV*1g?29n8*(>_Q=?wwqL)&y^IHKzNzLf7O*3#UJq}-|4Dfl>RxVK6(?wprkCA7#_{wRSv;ppH>&c|Na4;jqDAk@n z89mfws8tTUAa37Z;CQx1QKr(kIL0FIjwmDYD4#q@$szB1QkVPu2do;h5G(GVL()ca z^HmeBSf7Nvm#Zh5CSb>{HUZyU-m#*`Cnw88*h0o~O|AdIpH&eWBs+do&|@XSV?t(K zL~5Y0-NLKUzG1~Sh-yuAanE$ClC)`5tTgc2wl+2j*%kQX4tpJTt!z-(VJI;&ga2yF z;IAUI+c-Q!lpgKG`@bPZGo3NdFjR&D2Zl&tDbqg|Wo#qYO>rKqKmBRk^|w}Ck_6c4SI0e==oOj-ss7B~)s^aXlqsJm z3r(q7iJ!#<{wFOF-qFlBxhrponZ8b@kZQQf?7HRGIq!p$45vm$!NHv?FP!KcwZMMTDGV>NZ*Eu|dsl!J$9i~$(pmc%hE?fwQ5IXk7^YDp_(U_!wwLm0;LpwBc%E9% zJb0YVo)R|FUSYEE1t>ZEa+>X)%@Ivv!<3eb~n8jqt;lg~NT5^vz&=37K4P zQ|<{naE|`RQk)-)Bf+1JK8bKG3GS(r8I?zNqSlHK;Y(*t!#>gs!`iKdr6kA9$NaYiEm*PY z+FT@lba&-b3l*cioxzZcsb2?Cq1fqmPe~mHJ2B%*vweE!U?AC{NlH%wQZ^39_3aUDFk}zY;y;T4KWuO%Hir0`6+{-^6+*D);yVG&k%{7#EqRpQ+63bJ+o_A*4zD0FW z*pPmedzpA2(eE0bnU6wSRlC(I=&(Kc%MK#^>N&=AaXpD|;u1c^+2rWd>hm|sM#=Ec zeF*>sK>EL~=l~l-Cl46w)-wzQKS3RX*z(-h>IV_G5XoZm3=`FiF|o!n!ikbMnY?xT z`sQiOcJD%%>q^3Eg1&z3E}ii)UNV8AArH!3{oTS5r%de9Kx;-Jtb%BGBP@-Ar(&%r)dooP5k&p(TA=}zXBhP@8KhMP>;-sRr zum4CqK4Ej5UB2{)ckIhspg$ww0us2)w{^ocN%kQpA0cxA^7!syV?KU;im3XBAxa(^=b^ z$b#t^WJ6{eIa1@ZdD;X_26(-D{9(efUO;FSOqIakgc}f6k03=r{86o4Ev7q_Nh4J^ z70#Hy-eTa}k&p=Kgb^uXo-SDKu_k15v!xZ%F`69$(u); zjIlxN;sZuk;vGk+^lD~e2#y-n?HMm+DgVum@b5w14?{s(Zku1`7Jua|JW6Ef#^V-M zsWIYbn3r}G0gK4o138*fUz>BgYGXbmhgO2d)@U8`#v2FroLVG)>c`kQAIsvwB2G2? zFrRcU2>T5-BQ;tp6wfhS(40k(l73;8t{I}rtt`AsDoK%$*p z09Qt>gl!Y2%0gRsjjy$V@+3L%?@6*cPnc18dvIH{VGSy=wDGO>zZzk<6-lFoaT5lY0Qf%Tv3W-;cx<07A0kFY3A7vUe%Bs|9Fd<>4hXnkU zh57=B$$MA)udq;QbOLR!7Bd*RN|)bM43@NHf(8xpm-wrN>Mg(NmWQ{S8GSP1aZgqw z4%8XJkVF7EK*qn7tYIq2@T?@}1Wy&#m^!DZ{&`-rZ>(G+_*?x#M;fMPn8zj;s~`I^ zl6q?jrD*j+I?~&gXjATagxfO_&cc!z2DFa`e$LV4kev6_5>FuJVoZN9v%A2hT%V4m zj{7#uat8lOj=&Xgoo{wWI~J9{e!Y9AnvK`TP#%pdRs1?Z?~n}JRAyHmm5`pSpEo0+ zSUzwP33zWe0B>OU^;Q@sm*+AF#MA&uvc%%ai_h&_6*q)IrutIQmiV3S;@Z|o8J#Sc z{~#y3gqozAEO$lNjbLFa zch+0PVf@>29cjQ@PoGp-&W@edG|G%T zxRF-L25EKTy&~CcW?APH;r({JUY%w=M>~eXD=+Uad_3^^NfR&Bhr~PK_9|qzTf7-m z$Ze%7DWCfvo3uEvyA(pT+*LTE^pdg!N>)^Ym%>cuqd_2K4|URt^B`0mPI+$*?FL{+ z>297(PhKAwE<~^YmSbFXyF_V;LRcv)CF)x6oW}DNchAm_9b$cbl2@u%%`s^JT0^3) z=!m8U626x{PNeJ3uJ4fZmJaWo?ub3}9t2&kl)d*%$*~1(!%C+2!7`~zD8=R?gRb9k zNE2+%voEp+sOhtL-V2Y!kFk4o#FgCSXsQ_pQ!)IyIF}th8*1)d5sue*1fwCA^LsW& zAW%%W&xQM%ZOWwtoD(@rt7A3lFPUlX^^v=+8rz_{F_f+O4v{Sb-Vptis=E|+EO`L) zk~Fq9qb0IF`hcq?HcLw=e>ew)yoeQHQ(jhEiOFwwMJRBWSWJj7cNdPySIHe_O1zJ8 zc$3NFF7*F1llpu7TKQHWi#_(%vE2iU7ui}g_Udk`AdXtAqPl2NsafLvr?;|BwXxQU z2_v;e4JDnQpqKtT<~1*!4FE8yne=eoT84bLVidtBrcoF z%u@X?+~!Ybc5%_RtFeEVz79xA&0=ofnld1}7xarvI5WnhpWqsIrq&^!O^rF?iz@5Z zJRsHeauCcUkK;%c7`N7fJJQ)59@HK&0F3Iv*d^yzkV)$iOVO>F)-fQO49?u$D52DT&sJ7E6d1^Q=6pZMtVY1@rw}ZLm3I34llZ6f+I(Pub!13+asJ;c%QFJZR+TK&`p&alW&3jd2U z$F}W25m1IYfm7}v<#C4RH|Q#LUH%G{`uBJpsx$H*Sq4iz(W$0G4do;@ru%)Uqdy^UEqagI zF~^ui4i|#I^woyZ9Ft*Oul%{&i;e(%7^bu)008?m)XOeYo->%yKZ*dcq|xEd~%|aB%LN(p|{m0M;|dxEs;3=hUG2+c*@P2)V#DAVhYW!m;6o{&r$|`gix-%&%LwLI%`VRS0Dr}T$gJ|; z-j%t<`F8+1m?=tsLS4OCgHwbFrvUW1D}mrY0@0s~kPsZ|M^7lypyR9X|3)&tJy*pt zccG&P>UZ`czp&Xpri$z$APeBP?%xHii}L46!?_Mh{%lT0f#ZijQv(eJX@(>xa5N^$ zl-0Oh#*E=iJWLB0K(x`Vj4Epu81D}u+DY+|AntC_n@ZKPi233Bw^g6yaKF1x6QJbG z;9cnL!XCPXNiPvdEQ0&}=Wt{F7cbh&yQX(JMi&ZsviU!eMuW%M0VA@P`rkxTjHI#G z^v+F2bJ4x?)hAZ>PMq~lEcenV)zmkg@hyHxYZv8!Uh=TL@eKhc;4pHmjk2#FknN^o z8loHnOG(%d))lfa(OfyFvEdO1z3Lymzw`&HV5hA{hQ9R5vM{AR7{c}ok~d2ZxSG%D zir!cZaSAf6=j0W=u&RKTbhHJOm{n-Y>~G*2bG(Y5n3a{7m)B2AC2RWNj;mwv;N< zoZaK{ArRbBv3`FXOsg4srP@dLGt)D|RBl2+JhpXNqvQ*94Gq+?GfAfFD1md)b{$BU zhDAyVhFDvnYTxEzyPnq8%wFMY=TgMpD8*o`4>@c`#sK(XkfduI=^soMaj61ivwA%r zG(_M59jQIE%L&Oxi3emWdH3iIN`E%d_xJyX{p?w*S;XhUmW1*|J=27G+~l4oM8WR+ zQ=VySbP)xC0CU1Y&FwC$FIt?A53JwdmGUal8mUplv=EYp+p%9={26P)^n*z{+EYJg zQfu~G>j17O_aTsANNY|`Oa08yk9Y~izVK=TXLKGD(hk-y)vfL8jt|jla@8W^nO+`_ z;D#_*^kJ`s)?c}U8-6|4+;q-91^#LCF$XfpIIOGMJcQW_{;0`vnM506dmoWbhFnov zGoKDC%TxKP=*03CXZ}a*(RfP|I;OtoLb*bJo$tS>i{}Mj_knQwAuL0on9mrw3vPAR z=3v*gCHjc&wE*mLdV5jUJA>viy`z@q(Rm@b;-^74Pz2M1@L=eUrr^ z7cQx;U%-Hm#t1MXA#CA`EbU4nb&can+R8<=Vva)O4;??_#&?9-yIxnHD~O9gy{68?m81tMfz+$K+|W}hrRc;AqU~_b3%^^?6#8C zD$S$|W=N`ugpC%|gW}9OIEIx3jVPZNb2TP&{{mooBOK!p7UA$I1(>dxQM=lEGN*p& z*%B>Xsg|>Yrx}H`uW~P#4)!Jt$>ygtthD>BuHp^rvXggqcpTdnwUc-I`_I)Gx|5h< zfQL;*%9Uco+r0kGfnGA6{L*>wRVe6|^K>Sx?51E%C3HMfLGG{J`N@+~Ouuh%uuY&b zpRs()QA;i$vCM+XGC|k}Q-LDJdBhtR5hvfplNK`oB?D}~NVhCc*9CFokPj-uktatUo@?4tBHiUw4nZrxPOVBYaf#W zCU=Bp0vL7ws7WU5lL89L7}laJ*G01&jaAl<)8L6^&OzKK4@#DHG}jo)c;kk~L#W=Y zF=G04b2~6^d7r!74`k|nj@CmXBvrAAd^x@yhTv?&1uXRqTvLc>?~>wixuKoVj6?f= zTb-ypg#`mQI@fne{*h$1D7fb&*4ECB(^q0_*U54IR(zn2`}!*onlb$w&h5xR%z2w^ z7$2r(eJ40rBA)qnoDVoONJ%4-E+?eG*cY6al@AGkHxlLlXv{xtm2QQJ9WsLE)B}v{ z!Ne{EAfuXMBd;iB`nBWSJ4Xr-0Px|WrO=Tj@}ME{-SZTjb$eH$SPM!7<2iejH$N82 zH@()4Oeuth)v0h$h(g|5RDgHOAL>=vHc#i+Gtmj)SG6ZD;j|mN*nBd-{K|li_=L|w zUszaXO6s4mP6W$cG!Sz%eVEw13Yz;N{-7>3d^&tmp0xw$Mhtdt#5cKD;K&x$b{_jO z9(EVuzj<3tu;?*Pm^vfwC>yJ@@a^jw8|LxG9j}JDHz0{-g9z5ZfJzOAm{d=;20Y~Y zFhj(a?*lyC23gFCq~NK&iWB6xn`QoxmXeeu5HqnUx>qJ^w(cxTm*iyZ>n20720Y>Y zE)X}C}(*Vy_xICOi`lx6}offqy>IpS$v3_xzcaaLi1;$1*XNzI2~Zl7)Ko} zg@9Lbs&47aoRXMcxe+fI|g1SK>fFXGQ3e05+hi}o&1w*IPQSqViF0I2&juVn5bk+s>d_JsQafuJ+NZay~$Il!X zJhw()Li!Yv(B z8ifqP00z^Ld^rm)OCW~nEaF=FH|VkZU_ru9m{C_5!$eQEq6hVWDy-st6P>xS1M^p` zO;0$p6$y0|H3&tptu^V1+^R{RC3p-4(}in4X+M>UjB=K#8!3Z)8jKAUF0~df1O%U) z$u@~hDCLVr8bWpgyTG1>jW8ZTMa{SS)K)YJMP(oI?E97}{GfWq718-#HHT`p)zPR` z>zX>!usv3nXtB#$g(cqS#(T~_<*Ab=LLev3iHyqqt=gh|HMan%1HEaE-bsiVH}bmNb zSnSWNH`Ef~o$ctBYRw7>ilu74m;vF{wHa67+uBVUlzfOI3WVw*;p-}{D?DaI?Gf|$8TBWDfG?#!mg>e7JkD9jp3}MOty^y z-iPlha)C36^9dPE=1@>;+>2eHovx#25MtKVwy>E2eeuMk<232xdu0wB3go@~RVvUb zH6qfb@@Z>&&`8XkLhIhA9)^9RwslcAcV!Z2@{}S1o6V8^9f9}-IrSuLTl2C5} zBdAfn*Ad`iQmAFIQs2@IJ;aSD7&?n3oplK8Pt~@@2R+Yp&<2jA-bn^yyX^S}w2AT( za6f2rpkV~|cV&rqp~SLk9HBu%0(8CEcU(Gwq=@LOX@8{9yksMGSY~Kn`*(`C1vV8f z6HPj9AED54=+>(e1HnaJ4ranPo}b7XEVx(_;BF@e$i!yv*9}S*Glw3#0hqU$T)lT) ze7%h-XhrFOo3}8npMrtRTk0h!GJH=)BTqCRHf1Ea$lUWhMxB$)B(u@Cu-^OcVclNa zN-HmFl7G=8FzXNc8sXUyjawYD?Ot7?6Qd$XsWDL&A5{pxYGYo(y<51N4)QG#2(RK9Ea^cF% z!$t#s`d^cW{eqvGriazdNmmSE3?3CxUqFnza`@yS;Xa4JkH^btU4&e~p+_y)Qjfea$+$H5JvSzP^OP!K34^+PQ;u7GI8FCun5eEV! z9GXPk#h%SXr^|?3GM#v?t!Kv~5ca3kWSe=NOHfGWwBfxT#Wl6bIQHehMPjt7;Gk2DMy+4!hxI{gaj zj8L!WW^2e8<|s1}1|dlbVpE$kLMTkxr!`HO5NrL_9g$?CgIC6}>;&fi5^ZXbB?ip9 zjhg0D3}b8ACihA^$M$7kz8jYo-lm77e{4t8`OBh4Tyr2~#GnKQ(5yNf%dRQCed~2{ zM=>&Bld(-Ap{{D!g5W9gwE1ftIYc39p%g&rbc3o)Uhp$?Yogs+LkeV<8(%-#;ySSr zB_6S|2CO01c`G(8=NvPrM;bYq3DbdK!$WS7c<_TtP+BX{85VT)Rx+KE-yWOr=Taz> znq0uFFZ`i*h)rEe=w%cOb&XzAnIEjjz{3Mbn3kz;(>9YA2sJDkre(mOQrt%q{@e?n zCL4jjEU(6C;z>QCh((!?h{PCCJ^0?uch~y@1~_W~z#6BgdxJejX|44r zCQrTCokExRG$xWj3ahakN!-hFY<~$87pbyOGR`*{=`l^m-bJLqxT8Ky@gJ$w+Y%hx z!8-s4I{tnd_9Ec)cK4O^a2*^kcqVP`xX8eI`wyR)swU0PVlF&+O()2d~A89ey-MTVq zMA0~y#r#^f5JrCf&EDwQeius=A1dXAsd`IVuUO@>!}-l)J$SUnI)P~eXIc}>xsU~m zZQDyNchiH}tK3~fzAHu zK0tN^K&w;U;-Mj-BB!r*gBR%x_{0pt7r6&9scOvG7@{J;+XhfCTCu+$L33G|pu-_- zA*k(~?XB=Vj|oEq5u%19G*)oPPWd--9N~EH6aj!0(?7WjOjVPp0QoiCv{_$EqLS8a zWD`A}w8e^+ipzCjFMs}8m{_QR>TYTBluozFpvng__srHL0?g2OLWzr&g49^~`$NkX z07|pvC-XI(Wi}$kC0X>v_sr@NZGV*3WyXgV9iqL|uo3ZUY6V=w9Z^zrV&pJJn_+d( zJHmejQurP#ib9+$l&4t;sz9H<8p5(ZZxPm()<)~WD7>ytaFIQz_B7<+jv+<6wl-(3 zxQ(zELoDgTluM5iq0u#+`I6NUQL_z2{U)Ptjid)({yq1H4`VR;b@ce|*t<6o3*)5T z3TtSn@(}MRuKCU~rw+@+eao$RUof^M!DB zp?GTFmq>!G;O^6lnYtNnXZU3nhE5f&;?I6MEhP-V@f0W79cmLlRv2Pmc{;V9WqkVP zGvn#Gdmdcnf;#=tk6C*NyY2Qy{QK%08ittP9t{)NIPSlHo_1RDH_dCfQW-7oydR1AAB8HMrLR^r5HANyo;&q1;p7yB# z%d{Btc_oVxATa$*)Wjh{>}-I?J9gp$%Knt~XA0l;ZtbN?VYCXJ80W#jwC*-a>;0($hovcK6Yey9(LpX@S44G#o0MET;S(mOMo6P#v-{vZ%lWU2Z)~2jaMl zc`k_5;Yfvad$!5A9iK!YeR%IQLq1A7#EA{?jnK2|ubn1SyN|jPG;)w>$ zU~0mn8T!-Q_O?`&L_p3zF*E+ani_|5`jls&Wq^0h?YqJ5aB+I{I?1|sJ=w6E4o*j5 z_R1cPm~N>MQk-=^J&YgWUbIQ`rFPjvM}*>8!xncVu=vJ#stizAq}j)_qET%mW+%xF zlG)lH5@&l=Y2_^?Z_||!&(4O{?Ma^~RgIJe za9`~)9CRWORwpCgGfc_xYUG$W;i-7av;{nFTH?XGQDdz3<-k!KWEuN}&z1iA5OI#M zHt%G=X$adcAD1!mV)u8`81q&&Ixg>f$a=KC4Q)OJKOzadkClz$cXlQql9$t~XHiZQ zdl<@?!){uG3|xUT#xmJ&t9e<=im?hgWslf+9nA699wO6a9VTZg+dqJOilK_BHB$F1 zGCY%Kbi=K6Ke}~@p-GUsl=DL7rg}Hx@@>>1Ifcj$0{vY*3pVNQPaqwF|IhapX=D4% zm|Nk;xY6lzm1nf?#cZkwmQLcqO4SLYGK|3^uIThUA`@i%P5&HJ4@OSGQQXmx2I7ob z+8%0EM9!yz=EU@BI?jSx$9pNog>qIK9;cJnEOze$f|KcN!pb1r-j>^yxIP(tme|MV zHWA2hMIVn{@fH3;zjEb6x0GG2p^ZOLhXtpt_4*yHA`v<%bzBvU@y`yIj*||7Xpcm!_ zy=z5&1AXX^#QL$)Z4-DL$NUauz)039{3$@F=vOjZpeUA)iKmg(Rd~ku-#-o}GYUK? zTv0aTOa?JL8ILYcW(97K=?&~EbTHeryof+12=Wmf}j4hn?B3S@l^nU`**apnB~Wf_+3vJ#<4k zRqdsVkldc%bnEWBK=P>Q4D_ws_%RGp#Z*WU-u`SsOm2aqf2DLKRxc3jEzV(kIrmv; zJ?n8(*(MdL?<&!BP*Z|`T0@0bHgVw>JXz&&!efX|G9~H)n1)iHD|WWmO-PnkH0L{h zI|GGpaUhnO7sAQPftImIF?pAfF-BO;F|{Rp5`fn=2OSZ~=L%YZZ`EQi9`HDG=y?nM zQ!~x*3Z1){EM;e(X7spGB$!9hsw5$0f?ZhxD4c&jz(VYYj)nCGbtzuV)2bpgu`PhH zjHvGSggdFdME|;AqskU>D!RrN?+-aJS`V|dQuE4zx10ats_TbYeu1|}LLnP2ZG)Xi zZD2Aq)0NW|XOni~i1Rjuz_7D&5O%32U^6g&v@;1n>d{6aiEB}l(&k2~7o4ysNJ^bV zd1t^P>wapuw!VYo-(5_b>i+XUKPf}bbSemf-XIe~nz8$<7xS5vax)7->FzC#NqNxg z)wbkT9kw3=EVzn(1_~V?J9Yk;VW9;nY*!p3p#HU@r90o>;Wb#DOXe_APkXfY4n`sMe&|;4G!XM zJU{Key68t}R#-iq@iXNNhyFIuRzD3|LNX4aitje=y1Rl}UGXPIH-@CoegKP7_S50;^+aJX@pKi`Tk_1wU1 z97+DqBl)-N27^yaOaR_qnoOMoWrh(9y2-t7mf$o_6Y0uHR(hp(y3*Q>E^9I4S9J{b z*K8*6@k4j>?YP`eRdc&c%u^9%bbr5VO2mD#i*Wy(a zz#SNm#!CmxT)2*9D6OKyvQHhXw0dElw2dN~?l9(iMNGqw1Y9YFG>eFC!0e+jjGH~9 zZ?msH`Oe56TE&yXAKZ1gi|-83+QhsUCsaqkRLU$0KlWrF4iXGKeIu9~EH>`WiunZeA9q8*tE@%hSA0|fHVm|NBjV2UEX`VX!^qldAM z&az#W%sa(C@qbt=rV{(UQx6pCt8&^G7pm}&uv5$nRpH{Pb2bw=d z$9n`w+=K1@8ae6GC35aoDJ0WfR*aE1ZE))h9Rr35TAY|$xvy%!%W8HENh!1)ERG&D z$3?J^Yu6voxo`Fov|e}LA?RIy=;bU0BSK&wH{m71=;$E78J*Dd!Vqyd)9d9L$L#TZ z(0K_st)YvyH~$iFWU?8IfO9^1tTKOdT^nWJ5G%QI@BwE9E6m?)i!Hz2*U+XZO}p}M z+h>e_WQ6o;S9(M%5d{Le@nM}_uwyE`#f)DWfa}EXz+gO{j)cqJT-e+3Ofd=*FRGu) z9S4@pp2RYnx2(`pr{o1O#WC8+sp2u(Rgb=!@pdq&8HsUHb*M{nwqh)PQOs^bDx)s7hC;#N=d z5}mNT%wj1@-YIDXL)a>MFHygE_XV#6q3Ldo|GAwji+mAS_A38ebr-P9LR0?}QnP|G zpQcbjxV$V7iAW?(5np6RMyT4PU|6!XJsH(g09(p~uq5lyrg>Oy$;e7Sdm$wT2dmg5 zhXD-*vwwUrUyF0!j&QMPB>6V#m#F0g3Rfv$lYqvn^^J~~@9-lQSSF*9mTaSh`5Di4 zW<+_n4d<&sMKY@oXA`fl>T@a^9;Go+wAC1n27o(Xz=VJS2hIr3lVL2QA!t0R9>s-v z`^&ZDkXDTjHM10W^?yS>E zGQQudAJCsTs(gM5{n9d|^g0~3WxUV6_BkMv9q*_>U@H0S5Hx(P-Q`hUq9jib_knuy z@8Ss;Z@Az)bR0K^1LfH{FEtF+bU7EmC=g_^_%D~6fZ~gT( zh%D~A9$obF=?%!4Q0ZqjNsFGcZ6OOc8yZH(MmsN=w+(=%mW{8MJ}sG zcF`wS2#Bhg_M?4mYNEvgrJ*n-NMzd{H68zfP_zO;;RHs*s3J8UYI`rnJHvn zc7GHjF|zJR=M!-~n-CalI@5JU0@>)?=2-H8{mZ-Y&5fM^q#uSvh&X250Qwg$mi+Ez zr+mcc^J>MQfib6p|J_pW|}cOz0mOH zj-T#w93%+ZLvX}6=pAUy0LnEw)Fg(CDbWx=qgFb_wp`R0w#MS;X>iP9ia`at8qqeIT(UN5+9~_U>2_@aAOoRJH45NPkzD ztl_YE@d8a?n<-H_Lov^<0;_z2q*J-IPDjM`us+nVxsoGFDV>N{LyhQmsf_i^X;kzo zFwh!efY`&`R+^y1L|>v1`Fec${21$JqI3P@?s~TXG!fAnWAl0s(VVqSx+)eoN-Se; z3O_4AMYFaD!H1ynm^b>1zt(0vn2WqJqpDVi!FV*jl_L-K=GhEsBlC}7nR`IS zm8E_$*&Yc_Iq_z5m>NhYXW?hdN_^g;2=3x zOAql$rG3h8xsz3Esbp)^tY}^t3~bv6!0`bG9gh?AmEW_wFRLfC?)kDI>(R|rt7X{A zel0wFTp*}6hccYfTull;B#DI{=PnPdohDEw5sLbcM(X`b3CbhAEapFev4mCp{f>4& zYZN~~knR$_-n6;^5I7D-w>HW@k6d5=QE&;KsYm}p^L^8!8=!P(#1oEHn9&_V?EJ~D zV^wH<-}KvVJLW3H<^Z1twVQ@U5?SN)90C~8un~LT?AEBXa2Mzb(lhX3(zDu)&n1tC z2=;ca#~Z)-_meVOeX|&yk07;TnZ!g)Xt2ooCK z9S5v8Y+%DwX6wCXR)yrQ!!ghP^Cj0X?`Q_Z1G z#%m6BCn)OImP05-6G^$da%#3L8B)>iiJngvzpTE>^xUH%y5Lnd%Q zmL#OAs$nq3gjku+*kw&Yoie(S&bd5~qw{z#9z*+l6<&b$Vvn$;kDELhZSsk-9+ZLk zHPlHtPr0M(Ggj%Xg^go^^|*2AV+;s&KmFZD@4ru8ZP4 z8M4>f+Dq7-2i54J-Ut)#op;-$1J6USFD1pjo=^|4fa6?(_oOrJ%Tb6wCQ&{%LC4~J zz*K17e};|XbZ+72e~QcZ$9luT;mB+}nC#jgq`v<8MHpF#FznQBTqjNao%kFDJk1Ua zr%o7N<+~0We*3LF-h6uqmf);Ygoi^VR(5-nx;{x8Zw*Oz9{Oc>Kc1}Ic%Q~UTZemB zwm1ld3cZ{R6{qK{1^m0RYEcuC)^G|A*)Shi=0He=ZC+IK?kY#}p6XUj0iIh;Ogd~M z_4pYl_vtkGt{-U9eD{AS<5sA<_6HX_vsj`@r-=RLjp`fn2GBebK$X6ez2)7d_MB%q zg>*kCn0`zC)x&tDJJlN!bsAlgYW?F z)48-x0V93BTo?TyapYCrPMmiAEr+6==%V=}3a$q@XM45CJ4+K1|~AY_c5b5mYeC;{an#z5WI#YkH85 zMVArIWOIx+Hp(Kt=4d9C5%X_N4A5LfO@1Iq&syyN-i6kUcQYRh!34{30-W2nSng(x zk_=&?jV>6-_ZMJ}&F3}FVSN_EluYwhjWth+arpCBwZ4kEI_^J@KJ6~@7s?C5)pV|w zR**Xh0IHP)i{EkA`%6jUy<8f|dU$xGFnGz~r+~$t4ES+v-b}3J9JBmD+l}A-b>$&u z4#r%}6%L3B>o*R81j=MH9}r|=pUpl>#vV=B_>`H(x^j*EJ1#iJods%mB%H!#<=CXn zOijPbw!w`G^3dK=SCaA=Fk&q>Ouio5u%|KoIPcD~ExS#b*X);GjVB*9*LGLQLx}PwY|cGsV9+8f7B@tJ3OZe<1z+WYVk!c!=Qz@ zOC7<6OfYnCc-7wZ)L_gZLG_lgLGx{O&Z#+D{=oE_C&K4Prg%yfynd1Zokp?W=qDAT za)zN1cg!B~>%-_RXpIbWuQiK}8c5~=u%S(02Zl$8YB0mbnQej@eGA%&qN6vdCDRSg zAf&m{`F#=*8-=zNOFrMgAt)rP4jTpXU7j!=GaWiJHc4 zsCw=3%sZiOG^={efpkI0fQ^Z>d&iG<3(*@amal>u;r#<6!7(VBvown(Ou6?1WzHnEl<>(>nm|0c{qmy@k3H+_$og-xs2LNQe)h zagPf>>?foi3ovi_+U+NcT=N00EwwY-RcxVY+dTG9)I5DUQYu)72#Lz3 z>mz{RLag?r|20IDGt({vRJ0R= z3CJrDVHayAX3jsBe_YQSq>Q2s>;jA|`++{uzes2;~0S~d9iG5ROR{Fz(nkYQN&<2w|SrWz4{<0aX|S=zZ-%f3P!{JZWKeok^( zZ66V-1%%-6VF64~z_I1Y~dP=0kX zY8NIxx$)eAQ~yrpOeB|$JUXu*`i0nQl5(rMc-_MaI2lcc z??0G@CxyGOar;yu{(l={Vlz*UgxPSu>F;7LL2+6!xJC37#o*OMtDY1}!R)@pMkItv zC`t1~4#M<`g}O0 zz=hBJ7VL>g5L9@VjfTjg^x*W-Wd+n-7L0Pg{FRL1E9a+8_3H0B2<>TI>Q4`1BX;Vi zEntgx4W=}|onvhN!Ci|MVog@o7J{p3H2sBL#Hy(i+?{>bq$j)Mf&k|g){|W)Cv)bB z$^_ujxe70*+4f8YU`4X5&z0M=Oj}prmJ>yvKT(>|;y8SW7{eQ>M8Edj*q8vpfU8B9 z${05bUMhECqnYT6$js(9o79rqfB7a(kU0Av&w@t{%hTQ_F)(9;Sfd4G(EzeDy7Uu` zJNGeWq%?k`+N?@a(a%9|lfhGs#lp|aveqjjFb1cyuKL|(ebM>inAX(lHYo2jL zXny;gHe>~o|E|9lOE`Wf5!LHSF9*5ME9U4&D0dj)5?(XS4$Ti3Ol+nuqGa_W_fDB? z#Cup3@*#3t?e7@jKt6xhXsJ>+4sPX=j#P{$9j&LwRwTiH7Q9G*HAQLq&^!Bm_Jb z^S<~|+37^9*))7`oL0hmvT5@1posc%t|T1GGxcxZd($SM!Th8~i=%6lspueDrd}pL zpqtNlJ19{R%})H&3J*6CFuM}r6htf(A`?iAhDMC1dJ6tu$#v5*t)&HAq`5rAjF1Hj z_-Roj9<+kKFPpQ_0aXKmy}>pA#$)=z-OLcH?FzS)Xi$FrQpl!v?K)(lk&Tb34x#lc zj}Fs%dhc2Vx5Bn8t;E-rej;vY>I;zsW!LH{55Q)Q!GW^6GmZ#Xw?%kaI{rF4QFVz| zrUX{D2?^@%eB-_WeHT-bRJ0#W5|o!`(@FO$L^;sikr?wuXd* z<*P&GpSAqTnH`2eTf71I3hc*YlyibzkO@V{B%aI%fjuMp3?uO%c(6+vrg9&`zqn-drTTX&?$x*DGn-G+d3Zd z2&xDnxvZ4oFgNFqq_40~zaS}b`?c$tPK1F;)NM|M6ED(&QWfL+U>>8*=x!$;94vP` z)!?pCeAMfYySxe7U`)p+N{5B{zHnVbR+r0&*^*sQs02oe8i!KZTWlk}mSXd8h^3}7 ziTNn}Q8xN~86UNMqRS1*eQeXrbJU2j&XtPGutKXYruTcnRmGg!gP3r-Nd3)cbwjHD zb`-S`+}0T?-K1FCxsj8%B^y znP>5FAg>E5gYD~&VO#|cYzhw?%K-mB0Kis(&<55`d%TF>cV|Chgs*Q+BsO{HZ+T_K z{r=pe{y-=ukz6$G&T5=@>VS>IcS$Mi%CP0w2@BxLBYtm>66!JxN@T%BFE4I^>B<0( zEN2Q`*c+pSNqmNDu$gpUNQ@7*BqlZfHy zF1D4B1V2^SewpL4=Js#H@9;LSCh8787LVa|0WFKk4>XTb+lj(-N&S2t8|JC;V@u8L zGl&<@Y-Wh##gHPJ&x)w$t@*)qIw#_M1M0#(y_h%BE+F5C8a@NY!-akYQd z6R%;?@>nWr2!O&^Q^_TEJy;gljBRg^b?r8mxFH<)7;j?h<1KH$b4~(fW{pFvb5swc zjS~uE`WxC7Ai&A}cVY`JbZ}xkpq=WukM&_U-h*&trMyCePXT3A`=%hM&VpQxG1fg~ z?$zM}<3L5z!H&}hY8)w$1pO1F<+54~5EdGZTgZ!`yh}StKaL9nLk?`(^SF77XG`Yt zPyH;&x>1z9lnE=F&IZg8kM;3z{9U*Xv2BC~&_VobV`a*>LwUkmht5DkN841X@~wJejT>CZ zPfFI^Wm`Er*g;J7=+iR$o-S?_)dosuYnhq#q}h)qSk#QjL}8&EQ-A;q93O}UVe=6$ z?Cv~=mItOwq-7Po!eBnH6pebG(Udy`{~&0IoeD+*+lOYvNdH0}ICMVxUORPr*_-6^ z%f`7SP$wu>f}2^LP_9PYg6F%cZdSb+tBjw9AKa9vyd(&*toufT&AcD9O7EwOVt(Xq z_kAAebnNw{@wQ{v{`H(kwa#AQ=k=xB^qD;BO5GTu8J6QQX!p7Ih(bgT$jsDx3|107 zFQ_^tlH)J3@y}1*u-28`IGJM;nfwaf80%YThA(STT;Dkp2CbeGUrwYKo*&Meyfg)` zXmEhB;Uxre6^aMP!Td(=HR#Xn?8({ce)JJ(Nq8`EPX->Jp>wnubQg$E`R<9=({K!o zPo4Z+z3%3!I?_-{6)wo~le51@`bQogd8Q4xd|{M6e#@H8ho5BHq0Zl)pH$# z1ErBJw9_oAj6d@Hv6RuF(Wyx=Ax=IafxaRoUpIpM&yt2T(e^ZelWJ9kBJBSG#6H7{bkGPwBSsMmnOu;+=s!_TjO@tm1by(Fh>u4#Rq5(;R3F%* zqH&0Gmp3w>>kF?(wP!O<-=^R!daw?io9T-VFR}ph3^$U^*+YO2 zKGvt`m?DBa5c*c^CPtxOfT-1d1bDI_K2kB~eg{kW* znS+OIUtge(NM{T})A@oLi7s`M06jp$zrDfm)-|6OkM36k)?p9ADLE=?(niFYW~ws& zDVPBVAJ{lNfWYJZqg0s|AsemuZ7!psUJGCeK!*JWX?0QQ=V7_f@)ziZdP9u9#5td2 zo2?7@?G3@c-yRsUKV<@dK)`!s8jL4bbpE8k$;Npgid@9aC`N2hsU=Fd!2|<>^ak(` zj@WSy+!<}(z_Gik01j!FIpVJgc2=6=hK)-Au7CS^G|Ie_kqr5tO4Uzz?){<&tpJ&P z1If?fGCVds0C3Rpf^O!CV7Nws*s(U^>}n5|P(#$afMpCp8zv~ zzKX)WmXPS>#3d*H?TRynwlKKNoBwhrik6Y4IQ3mCVe74&+dA*MN+)i`P0d?G?k``z zPBM4Kel)o9mC7*hA~f&7_zLOp!i=l%I2$pfh#VB-7@ z{Ow3Q@>?a<8<296l!dRIf$jwg6>UGY-o4ffM3kkMRzu2K1=nnDUn z)7^If-#{~>W=Jx-Gm^7+;2C>slQnXTcyDG}4*s38BZ6pC$>T@lq&Rso%Ev}zm#^CQ zH$G%;77%dwTWvM#*#HPP{iY9!U>lkVbD*Y!V3!!k>l+`&fj-21E14xOV6bvU<-UOfdnltEb-N0gAIm3~E>9M+=Jd($<)1peZyqeEs3i zIcb8Bx+5arPIEf(4-jVWigQO;8vshjs^c&Q`qT>goJI@NC0g6>o(LhRwe> zi(NrW7V%TojD1{6E}kr{Lu}NA1!_0Z5OQl@Bv$?uzgB22t$sdncT;gAKRV>ef+a(? zoQ80~%49ay?v5r2{|}I9zL7kUP8z`?S)ZfV^qps^g*vGV2P`j@ioJ+Yc9`^01;muA+W%`5d)UJ#y~ zJ%HoHt|S!>_ev4RGP*SD7?~eJZ#D2M>3V;)O)>Ic_>JNbGL^ewzGxrI*jx4SPf9;Qn+sg*i~A5PpJWKu_)9OL_P7a z(~eA3m|LGLBD-o9L4iAU_^GGg<8&v)u{o&!;%koAuT!lrmy*BM!rqD#E>JQ5!QS(F zS)@tF1rNt?rspiAg!bX)tQC|cL zj*|pmO`KDIV~Vf87aA{A3ZIb^#q)B@m$*Z!r zH=?PE+B!Y1nCTE1He>=Pp735eCn#TdFB}K z5?_7V!dcvLv|bjEqLbi7wS?x|xSN??Oq(Barwr)?ke3(zDVk!mLG$n0$AN|Rt>eWX z93n-c6tiH<=5ghesHy2=;G#`TOKb8&^Obh@gHpE>r(HxS!1~N{DCD?`1rEj)V0k=13#e^~6AvN4Aw3;yK*uDUSp?^%MM_g+gQ$n>DkiuZ+&_w@+WZsz?V!Vrz#=W15>&$^>;`#9|g^b<{5~uR%Cy zIqpV<1p)3W#_d>y@M=<^S)&xD$*`s#VLeiGnLm>5Y!7lvd>#S+EJVN7DzT?$b`sX^ zE|Z*Jef}KIB{hH1ff}{E&Vm?CjXtc$SvBWEx6r~fz8bo_)Iw9R*sUW~pP>LX^gJu| zPf?6r=(_F2L(B5lB@Nqc9C^l2OknO>Pwe~QjrgtawPH99$8rFE&YYL>ZL-yiP6nv3 zWxo~NG3e9Oo?)UwGp4TPcy^5j3Q4Fp@0Pd7NqTqh`s-Q7@slxA@EQ{7t8b zCC;rjcl&XZB~a$VD%s+kY|9Ial)Gv@X|GQo@vAD&tF+czXP;;-R{1_4exk$gSqZE(OuXA+~bC$izUsrGudeONyL zX9|IW3{h1ARpjeZnRt(H@azd8a|Ug8OY_P#vI5f5W4)Jex?m@nlwd5n4k>3r&zL2i zCsT7x`dv{#y5ODlb4@lGp~(2gxW+eY^pPe9?CIt1$$8J8#+8P-&9a$cJK8nCuK-zW z${dH)Xob}{zYDaw=C#2JIR03^KoOFKzt?@e-db1^r%wg_S_`S{reG;`gnbp$luVo5 z;H-U&B)wAeii~2=N_ch&C{qP=<>4!^8Qnk}fEa<7HO8I4@p?(^IkKK1tFB6xbC1R9 zSI*wuP$&U5PvKzlK_*aeeFDGmn~ij`xeT%Y`9hmRYrKm5Ou78r>Xb+;2XeA4b_Kma zQdQZ(wyknfIoKZzYq+=Lvi7Ka{A^raWj74d2>*PHz`I>sL%|d zQX2_!hBngSc8nto%F*p?>>man#Q@~Q_ zA;emiWILP!V+cn!$R#_VO=#Bz>ne7H1x1O@O<#XBx2C#8pMM%Kh>@N@;U)OH%uqpmLpZQY zM=)U_&Pa@llY|zfXH&QwVGAxbRady%&C(f}N-a68v4OQ2dzS+h33gPH6bBsAsIdga zmCynesZ&P>K#Uc(jS#`jtnoY;nsqCcMP;F98%eaiYP{PNh z^CG}bNb-*n0EV&iMQ3<`e|s>zFtOL0T@4bdXUE|PcM9oz{Qs~%iF8)B?J~#J6fF#| z13Uh%i9pH`v*4L1s#`B~0GvNR|E=FOI4Uhg>~N^ij7{VE3~{3L?KVQNoN;o9a^`A= zRoK2(olyD%M&c`8eCQ#1Wc0f(qma8M5!FqTkS1)z!_~v=00}9eFb0^lhv;t;bL>gr zR-1*ybYP|sbTq0GM+S+za)|Nv|8g7)J4Zcd6~;}w+d4OJIR_qjQxYb}q~jnxhi~s| z1@(F^m3UtZn_(@`&&Cp$XfCulcUP4o{|2s^eJWR0qQPv%d4xcwcbmopK@CF|jSE>E zTCYJ^)o@!EDGbCtgt0rTaz@f*4PnQq5*Bu8|46CuGWYYV@{e)V|=X<}me!Gq2?qP9vuU9lc@BHF6=7QL;N zrNS#^`BfI_8)V(a9_Xf3&Sdz@9`XoHGMATQKdo)XG2%5IrYj>6YV3ZJjZbg+Fs%it zAw3aDaV6memm=oD^_u*RXum7j>HeFF!Io?=hX=EJ|NG(rn)^jU`I1zrI;tS^{%tRF(x16 z7K-Q{Ly08L}O5s|dpl~;;{G$=&lxZ#OUv&)! ztX}lU;(y4L*|Fi6jt`L()+C9zY^-3-dJwPjUvT0IjW1DrM*Ngyiq;z&+e7vL5;4+k z8cls=e32`h2&Nku5AA9Bq}h>V)@P-2uf`2w<2G`j2A(qXh07U@hP)t~?4|h_4vhZ) z=zyizWH&zU&lU~9&Y4`2^Jjwg@#O0vlkmH{v~x6LC)YqhvsS0hczQUSLlo&US$4KU zI=`Uq<{moBW1BtJ_;i_WdD!tHQ@C{M`U3-p@D-oiq0PqNafaVwRTu=V@vM^$hbD;#Yeq&INBaD=8rnBqZ zMBI=vPY1n*O5!}ZOSR=hvY|t+z}t7U4pl7<)zOS^k<*I%DzgIhqua8I5O3&8*qdWv zQ0*!d044jU9TEuOa`Q$X-K3BON$QXS^CI*=hKMBTDlOTopA6;2?$-k9`~_r}OS!mD z9g~Ubr=~+9vp+0Q&kq?yV`ZMK75s?ZqLf+Khz!2OiH5A>7KT8; z(nx!1`H05sY+S9v6yWW*)}0nFtHlI*O~wu`k3!nR)x_X5o!tn_KM;-vQsv~6L19YT zKcoS@C5W=|_c^7@G*(pDHdukgFxh5yOQFa0HrTWCG&#&w2N1ymhM8@=3T@yqOp3WG zO367W^tnu(Rz1E<7^Aghg!1$1L^v8CJHBVL6b28RssII)%O`e-I201aD-896sy-(? zq~lg7iR;)o73RIkM4Hb0fc9C;%&~pbLeOlogirQ`WOi!>9fPXi7>?5OIj~98jY^-l zwywGigvJ$M2OA>sr89P@VHDqx(^+=8ThC(Uv;ll~QSa)o`cD!w(AdMMTt4etYxSAGtX4ty zeu&V1PN@AI6GZo1&VI!snlq?QS=EyG=ucg8RpiuDcl!ma9cO z=~?4=9CUZrJg3-3Ei(+N1{~7N6CkavSrvKo`AmU%C-9>+08TayaXn_dT}+9?W)7~? z^|-11!FgvZ^LRu+8Q|qf;#84xyCZArx4F3GSBt`PxO0wwWzQSwsp${$89xB?9_`%X z&1q8=>(FNuM-E!g*L822G*#_~bluF8PjKuz`#`dPdO#G1vcm;&q@8A%eyo}N8ggo) zCevH%-;*49jC1DEq>#L6vOd|Q!))Yh)G%}>Y7qF+A~hPPM;<#c&{XFPn!+ASr?Iv8 zF{>sLnnr}vNlCV@u|w0lPHXlbJsPW&Jrw{OHldMr@ucFBbhJF$oSEj~FJj+^h8c)? z3!K4b<)u+Yt7$erjnq5XZp;wy@>pXW}Jw2+knB?3=hwSe)i()|2b`&wC zs3bX!qfO@XVv)B~)W*t-%CtbGVzjt;Rk+CCoWcwm2?Rzj=~lVc*hk+ho*y)cUM^KP zM3SaK&CNhM+H8bp@=P>qou)yGdMGTa4;Qk7GHQf$LMb&Mz#ltbm;_03g-@NotSp8C z;M^-SDFIue04&^Heg7k3BoZn1CDtR#uz^CDvBUQ8DqJ!?Rw-oXZ#SgxOLropwddT8oGK_vn)vSc zijVfceGW6JNSrC3l)%t3y0Wio!)!(Qog?Xw6$|({wJG9}8aiG%ubMQx4PmLF)Vhfk z<>PW2I>aYA#PLE;S96_=3X8Exivh%6`8-VVWPqD%{0Bwj=JNCl*0-7Z_SR~L5l?*z z@~==zUgQG!->>>`kOo(z#_M{uPbNB@*AlDW1EsI2%;uey;1h8Qc$ z+LyNIPe63#E33N_#tH= z|LcQpjB8?|>hU~Wq*f+pbZ~D%aY30|@#U z<&FC6&pecJU{SE|iWRW&C)0=T0rO+8E}m?eayMBhSj4x;hYmf=#;LdQ?-4q>0eirCum{U2TMXQulZfFB zOX8wFW@9Q&L{Pdr=>95cFIep>13>c38;AMdDiNzYce-G{n%cUZQO50^!m0oT(QVd+(-`}VntO~n?@N&pD9v} zH_JaKK4N~KQ5?01O0s~BZ43-eus<#WIgGM?En6gy(Ej?is^X)c&?H z9c(4#i;M0ZKa__cylg}Ez7uo{wY92kE~RV<3u?sQ8U3A-eF|4xrU=893UY)6GF+Q* z<`bgCVq*QneG6sM5Czp)b>K#dk3L!)UeFJiub)-m+#_o-r4NfoQw|$fnYHk!fRXGr zj;UC#jyiNYzd4Ip^i^k6Zsgo$TcWa}=BEh1##IrLYGDtXhhR;Z;PMOaIk;+11&g~w z_3c)Q+jDuT%&8mnSfpA_5jMW(b1L){ePPeaJ=tHv&y7@R+T^1SY4{0o!DQ22`? z+pyE8Lr<@pDs+H4?Cc}LWr7zWgH(2ze@Zm6Ck77oQ*f-U<(3fsYOBl=6~X%{3(**J z{Q_0hyQ>Tv;VvgkfI_TFy$!Ee8TOu?q*6EbIlx0GiZ_ro8CN>V9XHt1;!2n+@suO8 zaZ2_d0ULPh{tVrv&5{MBY{1ovVCKShL(q)o_{xmR@@=05Itz{uBUdrK-aEd{?M>yd zP3q4`6 z2aeK}fBpoy4dqz{Z}w=>Jk?k<1H;&L{(AJW1taBSYA;$K82WK-TL}5tQKDgl`pkIM zo%`SA5s=HJOnf)vWl?bP9SiY8|E11=asXSYep3`C@>$U+Wy&!+G}x$bJZ_L*O}&E= zhuuFAF@7?ZqlD^xKYBzV7P&INvg5U*DJjO+<*KT})vx?rB^?bwY&ZlbD)=D^jQ ze=8w_%d;)|%eP8Yl|oG7rtQxCYlof0&VT;L0M2jgIdIZ_;DB2hA%Ez~i|ZJTO%}S8 z4V}8es$fR6V0zU_xI&`?tdR9|$O0GoeDx^dKOL(LcxTFP2qb%P_~u($dh1#4w+G-3 zjwHreuhc#L8`Fz-Yuz|7M^Sush&;yc?*M`M*t_i;9D5ohh)0)R4qzHv4b@fFX(1{t zbjZ)%H2PhXkRn>Xy(VmAhk|?DiKB!mM7b^lKSTB0=m)7lwtp1;*hW5)hpo7o#A61p?=6_>LN2jwa;2Ma; z96hW8Ox{^#2MFrt-PJi@qAC$#z;MX{O4ZB(lJbuQlr439?1ovZMwsN4d#op~`t}{+byZB~gU738 zdL>%|8Xa$3Y#6KEAA*={UG-mqc?LRq>bgyMmT4I|f+s|zl%P)x31>zB-BRrV!vh{T zn-AkneF6jdYt%$i6qYSDtudlz(=O_idnvtxF!#0(o;pi3=t#dEeF^J(p7=wwG|U4Q z%*2pmyfd1zW8yA3WXZJbe{K?E3yq~u>NoW}$$$E$& zP3u+$0iT?1@u=9mtC}v@m2p#WXVp0z1Q5+V6n#g{^9#ehIhaZ}<36i!_s zCsogep8w%VCPArdEv!bu-&>;tXvl7rEHm-vYDiHxKAsf&DYwC9*O%YVQ{9|qysL8Z zr-m083zx1Z2Fya8g^(*feE1I59EA^cB0&griNWW^io~|>g=W_QV`N30(*hZ*MgYWj zu1G!Z+2^bf9!>h0#W2znfL0=>?zirWt%#oG!PJ)BdrVR43bV~hRhYc*Z8BDHd~>r3 zI)4~n4Q3sJn&H6-zq%(k#KjXXJjU?}c|ymOtfskmZ&Z+VJSj;Wd6 znX69POsAA}gJ~8WkxuMkG;SA7LrR8FzQOQPD&;!Ut|=0I6WLhqVHO*?TlgP#$lfHu zm51aDGMT^bVxq2`^tY$bPanRh3vB zuf%WdTv)BDn0O~gLUznE3I|I|m4lTKjaTy?{zX0hX)A}6#D`uOSrTfE*C{%E^w=Q5 z-)}5hZ_L=48=A;C98n>FBTk3SgsIIBHeN7EQn0h!zHSDy2$6vOz<>`Brn^KabC+IB zWq)jZ3ZP?QLagFZy%9R~$O-(-bSldpBv*xT2m57{u4{xk9g117d&e!hwLy-$++;0c zi|%7iDZ(q!g<8dC-uWG2*H^bwczac!gze#6g6fiB&2krCdRD6F|n1-lTKzCc)Ip2AU{*la+vr|1SqTqEc!f z$q!8e_7fF@KTnR%sSY0wp9?Arb}9XE<{EV2ZLKXo&WbJ5*>xb%B@6#OV zl!PX($_x&Fp?ILni3{KWOK}TYUUHzwzKRnL$FMoj>5R7*o+@~T=V3Vfi<>Mbgj@|) zIY5e8zMajW#nz1Tv_y?*C%-N5C(uDlXdK5qD-M2vaqAnb-m=l`@rh#gM^ z5__<FO#iP}%$Vy))J^Oj^u6(sgju?gX zkY0H1L}m_`cs~%ZiB-{$?LB2={dTxMOQ=95k^ylF5qQwn7eLmOqu<(~vb@sMACW6L zVWfK%;T%C1iv@wsdc00s&eaC0lU#CS5p06Q3`l}|?3WuyR>?qy0Q>INBkIn{d$nln ziIpNQj&fm3N`BaNQ>iPpMNPOQ6A;)h@?+%s0F83WnjIAxHJ9U~-*jhFTjslJEfToH zn4H_!o=g0+mA%HD%g957hc_vIVSc|$LDO*W)r|&I=?6%G*t}(j`wlR70|I613MnYH|^F~z5 zg8#JTv~xy*)ymVuE#?r9OrFGKfkTE&HzH^_&8*s=t;4ieggdPGrggqVH!nAdC+C#b z0m?P-BkqCfSH4KJYysmpCtCeKDtgR+8R8s7C?tD6|PD1~%93u$}80?SXC;RwVcGob6j~33WgK>nq425|&f>g0fKfj@{ zL~e8b^Q4ndd|nIDxlyr3FMht>^Z2)(jkggnxd#&-WPrA4FlnCePUVh>k!OoAW7OEa zG7k`Tgky3%m*ibJnAGcV2Os!5zS9RnWW#E0$Io?>J^%2x06sFOZ{;f0d_yC1G;kPS zGr9Ki$Y_e1=QE-|BP1a37|!Xqg8VXa9{|1pyI@+boiRJv1;t$j$ULnprWZ1n6pNfO zB$>6p7`b%+wV~h(@EL_6S34P5OhrlGEfbui*vI#G?R3qUh!E?I41W#Y4Dwb}Mk9jd zm5iKbyD*`LRaMj7_6@GdHyOaSqf`5ZWPe`1B-q0TYjUu>`#;0}#eOz(F&!eoi5#_n zQ$8f$Ge;>tsh1aBRL(&Xb>c3-xhEVaW88>}y!aL`p<(q3I=1WX(&Z(MpNaYdC$$g* zCzJ$CkSWgisfg{3i-(j@PC^j>zT_yFE-xZ{duzl5ckBmM`-7HB{+A9AO>jk3D&^|n zaKVtVgI~(at!xnua9%CW4!o2i4fxr^dMi(}GRln2et#dZJhnJ2L!xM#qlx~L9-fQe z&VdA@ctBCOixVCP)eu#2%l!?&4Hal7XoX%PTOFKK&VkayUyufn5imqYSB#X1@{p=} zqiLA9{NsfW7HkcV4r=B7;BhR3L>QJ!S9`wO)jn7}X-DSIx=j5W3{R($tihp1N{I(^ zKu39|uU=`*M4IZ@zJLY$mR%G6dM!pe0eOcdaq{GuJl9||xDjJ$8c}ZvOYengB+IVny^O{X8xQ5QF zX%b^O95S%#!mHp!A!b1QQZz9IHOFbR;8p)pJOtqFhm7^p3|Ocbbmiq$p}9AxR+2PI z;WAN-h?`m^XOlFO1wsgJqzi#n2%}%HN0SnKSOESfe6cvtX*VEAh(_*+t5~KD}+% zeKf}Zp$dQ%@hVfJi<4v??i^{fzT$zk4EmXTq>8O@@nsJ@Hx5xRp->(rM!~gA`Y=YF za|S-#%xoiwbBx(8UfZgr#ep!?TP73kM-=~}3;|%7tr9u&G81VF22Fa@vP)6SH+z}U zb9eEG0F5!IHW0pvx%-Xr%#A@F(i$)PU1iv$YvLw3V1_)aIrwWEji^C@NQQWxsJL)Oj zfRRUGjINsg#a;q`D%6B_Eijt)KQ+^#!T<&PpI&QZ?Y@s)s_lk0NWrAnZQ*^)a32WwFq z^2V{2R$mBzo2S=71$N6O`OC$x+ozLzb(bF!|1vb$I@~$qKRRdyYumTG@MaO7&voC1 ztza48i5v5dR{mX9T!k`_9jv~MW*#?>zutEON54sXwgwj#i8}hGk6#n7Df%r4O*jYn zh-G0B>u%nv@p~CYvp6yR`JVW1lrRWM^1n{MG6V1sNPR~w}-?1tqwTrlRgYz zPCdQCx!XIN!$fOCvr?SN_^e+^pOYQbr*@Wu@*>~j;1x=`uf){WI0gKoth(>@E5&IT zpJo!Y;GQvYutERq)fc}Q)eS8!0FfXMw^dPyp}U|afr-r!C=$Hl)Qa`0JElVQ2Wtra zWFs6|%&kLRu%8BobrBa`1&Eb=6=TmBQbVLE&a|YS?~@KHaPvYUwP}Vkuk_-FtlYS} zx>KFV*|$5PdWIE}9yO~_p&*^4pQKzMS{BBy_$sf!ZiNDl&8Jlq6GPP~iku22f4^{J zM%QV}6Y}BXCe>8QSD-q%wbxaI)x`Y0RBCZac8 zODN>H>HtBm)!%)Q(Lw2NSr+S$V=kDp6s%^gwNH+vZ!Ei$C3ePQ9M!GPjF$GV7FlJQ|+^GyU$YN-TVlU>ujUQB7k97qdU3qB2HFVOxD5xLsw zkq3YKoVk|4v=ufw*G|>-%@~2=+Asyr+FH<RKi?nAirJzaP8xRQOA164F2OvvJx}nfX~lzAQE6V z<}zQWtNFZSrO}IS8N=z}UT*QY>shaTg;$*!N&Xt(L-@k&$iIv!$LGC5#2Cdyc2gA%#T*S$5 zyVrS6nera&TDUS?KiNG~c}QU}y0_9shP^~gNPuSudbBcVX`jfc%TqL0+f+5>Kp(`Ya_C!o9KmO zIeH^hdD@}bhR(~Vw(KfjCy9@7FpXt|P2)H;CYjD1R68JUmS@-Y z5unLP8}~7BA{0w<*0E4|Va)#NoYqHB5k?+^TL)yj?vI-~V`d21z>wmIExGq2E<6I= z%4$Wi@%(iN3|&>8+7d$A83uT4z6}Ti zg~Rpp`+$S!Tlmt79h>M6W%{NV&s50#XQ}%!fliqjYxrk%3F@~AF@Z)bx<#rPKwZN1L(XfosHcF1KeBjn#jDIYxG zk<6b}zm)-zeFp?!~=VqJTcXfW1&vKc*+H9_a>N9wTyEr?94rVXE*7I8@H zqQ{;IQ5XS59M*&WG8*x8s%tVt>VBC2f$XzVL-hpY-)*Ot{l=Xk{VtZ^3*~-!MB!!{y?@4T0aDm%^XXW|I zaBkJYF|U#ZOW?~56YJjg$A}CRR!!O>Fg8({#Vt1)5wy* zCyf_C=fw^u%&gFb$2U1Z$sjf{%QWvc-6uQZS1@3+ zO3->8_|ITJX@B?<2xA2$ws3C~D4A29lL4<@_GHB~T!o8ENhI!yTE@o=MdTjS{<3KO z3LgWu2r#CKOq`0u+EJ%;G_Sg@AON=gZ=CAj46Cdsfns&>c~0^|0r4>XnmeTfHNlMW zX$raIbz54w6{&%#Ld3EGpf0ZK;sJBK$EC1Cs2TFhH>6qM$3w=U5n(4T zubsDbC{XHx&QDi5^nHu(680WSjAvAx9`!FQ=#9Eca|$-umP_(`S8{eA!sSn7F;Ttd zU0W{q9;FZtt6!c==I12aU0x>hbEM8X5;g(eHj}?=`W{Gx$L0iy?Zp7aDsoeC5M#bq zO=ap@{xl(oXC-XC*ots?jGT7$e}lElW5lJkVgm1yZnC>Ju6CKFLx4bNDrr}GsKa`gyxmJb4asBp6 zCJ5r*$_2d}sMwoi=HqyN{>_4)8#IC-xkK;9oT6$~YH3^u z5L8*Tnd7W!A5WS_Tw!g>n9ZVSwUu|B=qgB9TWqMzG^Bt#qT@0URMkfSd;<5*P3Ox* z6NvfDXkuebF!Lf%km_xCh3MTzTVQn$=$D&ZCy$ii&^BEVc?GV1-TCQor^sz%=70o9hiDQNQp){!{?|9S#B^f+bzM%g zX6DlvrVM}MIJI{S366uCb}qSj{Wz^0fO_WPlFDJefdP1nUpl#)ZyZWx z*BrpePU+W$r~~iJdZ1>Db~3*}uCtMec25ZjSJ385YY&Nms3)~Kmq%N!XEYJFht zeOY%Z`559kb72{_oH`CR*8SxX*>verHEXQ!eu=(h8JR(b20^Rd#H0>`V9 zYA2!)V<#-cfmHZ(tJzH%?7>*KhDa$YDp9U^gJHZ%>%#+tQ&xE}0Pn^aQo%~fuAuke zd?GcQ@rKVx;zg?L(9WtvMo#S!n>TT$Dui*oD?B`uLhXWDQAa~(!bwlJ0q7lo)D$Fn z_-J3zQ?#DlJ)dN3jl(G&Se|9D3fqvUb;>++PoSd5x5>$YJlApe7+0Fs9=cm70d%#a zx{or-Aag&yO-8);{3>_S8J81wld1r_io+d#GDsL1Er}QTg0r>?iiP2Ic$Ie{T*U09 z;u$RHNrd4`6!EA>lBzbX4W)wC{y3-9-FV2srq%eGcP_%{4SNPXv?&yrsq~Yv7Am2i zJ!%|iW(38nd*1TLb2^O@PaFs`l_qfwK;k$J{lBa|59w9w+$M#zC;iUMC@%J9 zN#XQcgCMQinSeTHNG=lF3L4~H2Rxyxm{D=wYf3wGpd=abtNFSKbQ>8p5$l}>DQb2# zY7uIdV%G(ZYf14}k9)=jJWFEHv8^LbuLLmBhz)ezcq|zkY@EJdlMtw^&8tMEP;&JVSxJ&~Ww z`?so&Mmj;l%is*Xmhak;#;gF~ip_I=26Qiss{ztcO{Q6>-Bbx8(mEa#=m(Vp6eFj4 z`!sVqt5W{}<}4Kxh59@m+dI~L@pyu?S zZdT1?hb2Wl+^gYmrpqKeD6Ll&gX>S{$>O;fAO|MOa~i;`)VOsGMG)V?m}DZ8bH3rb ztJVq#e62tKSJgJ6q46)l=n?ok>NxbhDT*!!454$fU zlU(pJdawJolJ&j5mq24T=P3kA-ILpm_tq}Cy2TVLCLW9DW+nbm#552JmU0S6WCs17 zS+8osSn>@NYPHC@PJ1}d7oeO(VuV86kJw7AjPybJii?`< z;9=$MxTU{__lYT8k8SJ_SQ^k zs3|t=-Q{+XK>X?^wPc@gX|L7V!OzKQzgy;Z)lm$sw56&jh@iIiQRb3trtr&iHGJEO z4Tkj@*V{vL(PWGj9uNxC3ldE@MA|P8(B}pfzGYyv>uzIBC79r(NJ)&tNFfX!e{GZP zhe>u?FMY;$M#2%zSzzoyCOR{4A;CYaNo|8`bYZNPJ#tm)mF$(q>=1IF3U-^WDl^R9 z{ZIATb4;x0nE6u|v0n+X2Pm2iu?ae_$>Pks`~CUms(|m8-r|mGp0r4<)Y-M8a^#3M zX$@nglhgid{u=>fc0ywuEM+AS{*;G*B`LIgDxd(g!tjRVB8h25Btf&s;GoN8@OG9R zk91QmXSS`Xc(B1~oc;Pr?g3%s?Y_!l!d-d$fJ-+wt z3xE0@VY#rYEZCsmgxfccUX&CpQ^0kUOI^?A44AD1so9eZV8K0VOmW)xK6=W`4wvRD zN~ewc|0nL=crqJ+y_E$Yh<*ViI|I{c^v@x@o7XAicNGN!oXjuOvz*E_MC2*XghCd% zKd^8I0DqAxbaX#pM&za?fWbncjqxW+=$Ia`NA;?dmsu~$PTk>o_qIp2L}nm;X*f>C zi6^mOx}^%QVT&!NpJ|B1PBoXsEs^BCD4ZFedn3z}omU?Cttr~9Q+56*EfgkR28Ojomra!v;-AZ+o6NX0XP!sDwJG^f$5zP6^P z=B>uw4o{C(gIoWbA6WTQx11&ntp0X0IIL+8p%r(uUnDDRYnR|_dw46)!n{vFv@f-B zDga@!B~u%KS654W^%#QoBASePHCED^2;yW4EIXQm%eeot8b$y&K*+z1q6*<+bJ>gG z%13$tbc}MRh^%t2CA}-T=8Sw!+l#Dv@}6+^5J(79!Y1ut4Rvfz3`O#BrYvZf{iSNm z=QEcBW!k6KD4b0!(WE5?Od0II&Dv=JVvHJJz!WmSGEF$NVj!mdr5-h_cMG|}khfLz zqA}uoSFY&^e2lKv6IiSo`uOpN|4)9^X(~j@6Q=e4uvch&i`_|UtX6=TG zKB?1^rl}iDg`0ifjc#s)JuA5&U;SxSf!A|7?6!}VYsm)(#&@;L$9wFV&8Fr{0{)jz ztE4964N~ii5#S-3?bcwBxzYSw`f{*CpIAQoM~uv_UkE0Y1Nzm;w3$$8uH2d$B8x@! zOuqVoGU!}YOR5p`+8L>_f)9p)CURPP@hWiocxk|ET09Aj0$viD5_ElvX9|o+(5}FS zoEAMae`C4mf*%*I0zVriz4sxO-;lC|E5sg#~na!Xt#4v zPZNCB4>xQ4v-yss?%1#+8$I_oe`Q$lJ)b@ zVq4)WX!_5468-pK??CVKhw3$u#HX7TO{r(a#rH(^RcP={RYmFh1D7I>N;&mZzwzFH*E<(Y(u`K~eS`4_yR0IQ)ctRVZM z3d@pkw;fuGdU8Lq>$S&GPVpkrPU% z52*=7@Q(Pl#dK>>i9TvbZn8l6fc(<(aYrrB6sW?{mBU1ypoB0CFQGW=&Os>vx`aw# zRrot-2xdD6gwhSl>#9iylHT88X>C}ZmpW}gQ$g}9kseX3%o1AI-N#*HhRq}b{JX@* zNuoJA=j|1PCEX+D2muklk^C@9B*>eF*>8}qxiX9`ZgPkZ9d|5bRvjh_2kA`1*#%P? z)5htO7$GYwXG{-E@>USnc{Z26?Z#Wv_IF?SJnQfgWDOK6UyPjPGoj}b?M<@%rI$o( zL>A)8g#3slZ3UW~MU=~p^+*NSj;K=?oglR4t|hI(7XG%n=}-n)d?s8w1vTZ+OY=HY zJhCfqjk|c$#y=>uMsEfGS8y4p_Mk04sK>@ceEFZ&m3{=fMo~+3ViVTKT%HX<-G_)W zXv-)G6f@f@qU9)bm}VB<2X+N4i9(vWZZP5Gs5xdWkK&=i{_`gQyNKx8gVFm~f(X3X z?IOHSKZNyCh`|5H8|XPJV!P2~qd^nx7L^OIuHA4c3VA5pJ{fmxMuZXaIfYuE=o{&! zp9DR=24s{k$}5S&^#MP z^)kZU_^?sfsA+4`;pPZR)N0u`{ltL##AvO6xV2tqLJ9+|r)3&TX(Em+wT;3wn{+O? zjPm6PZZW}ZDS&?PwR72+j($MY@quy6mH(H@I!XuL26ge;+~Wq`>qn`{QR&^(0-zwen1aEcku`F)o+D2lYUX#` zAoS8-2=TXaG(Rd{Fb+Sq+dZ8E@Q4|<7YcXm6v`#p`w9Hu<`(=3~)7jHzDT{|{Au_K1Z&|GB5%6oTeC(I~5=jXY0V zEsSkhSbtaU*%V(q9*_y4Iw5)uqSZr_{e$aI6ApJdZ0mO1Z)dis*?Wxy8#ebNEwidv zzW!~rDN5%_c+bxO>dAyNX54e-n^h=Emx7P|qI^=#<@Di|K8xB)=);%dUcj+@RMF3X z!IHRe4aXazNB552LAen~yz8mo!`$`1 zAJi?+G1qS?Yd;GhJF~k8evTxpou+PbQ49vyTOzX|$p5gS<(>0~CGUW}rhy}TBbc{u zxmVJDfYiZ(Xe~zw)cOw`Wi#MI({nmIMsr!gr?7MAy^l_f@MjYlWymi#v$l8o0#K5hw`r zip8JSuO8u>0LUeLRH1)OWO$=9J|J!*Z(JU%d;AUUWbYlb;1v;hk5Q_fk0>xUzJZ3G zfAiz2YLo&@2Y5+?$d&)_{)GnMG#n`Ui*(r$1PudZ!9Q13W<{(cbBOznV>x&mil%WqCBtil*RZ&O5xCSm5n(d@Je8 zW606=)6T9bJS8h$IF;u7p7>&dN7LZdGet|`AX9oKog^ItyO~!kaW_$j>3J)wm?Im~ zycGT3LDTQZ#EQf6Y117e2Dz!8#;vs9KN7$|us*qaxl56E?wO*}Vu!}{QW(+f4p$4; z)&D@h5tF)eYl~n`O`rZh@~q4}G?MxPC#u=Z@|`hO9%mSf*YuXHcAEszlt%@P%4#t2 zl@@opDeV-kt;?XSI=fzZXZA5*ZO<7So>KlC9kK5lB?h<(Lz#b9p?TKYt?AGvBMKhd z4oVuV{IWq+0zTb@PtNVvh9YyyID<+%t}*xpyJtr#$?+Vjn-MAY5mk^pWmzpn7UeCi z0+V{U9$f%$fB^ZTcnlt2+>|12_{_~i^R+{V0Vgv}TL_=q!shYGROP$%C^&D1 ziZzIfyl5FL!+tYe+*5qHvyeuqa<%!PES5=^l9q7BsAnzZ&$rne^)4CmZhGaQWy6g> zqlE3h=716SOufg36-)$P?fI@BCyo=GX=Y=EK)3%loLHGrCnl9P~At3edlfLp}2(RFO|T&!TD#Vr-4PHE)HQ6LzL ztZmDay=ZZJfb>)pAXlqR8N9G9hxmbbtrp9eCl;n2RRqa0YqlW@hi!!m%7?T(cQjY~ z+xBCOpvb%lZQZ7B*?Na1cu9`1rjB)WXrg%_ z3MX3LdtGkU{tRoRM&sVk`<>(cw*v`Jv-^a7s0O$d&QbO9xPsG4gRMP-pqs>qKhxsP z4Q{u(ns`L&LKIiR*vR>m5%ABB;A<3UF+mQ@zzNtdewMByt`+QNfi03@uB2oF5BFO)(1!aOv)o!)jr(7J*gJw(FC6KRo*xR zb9hj4u(6Q2i`e$x&Qkm#Vdwbn@18<26VzkMOaU>RR@+d|J#_~l$hz21ytw^`KzYRCUn+j?@80MKn3g#UiLJM$e7I~5joi`~`B762Hwg=zC z0(#la?T@T=iOtq<&bWE{r!mnGb93NIl9B`r3+sw-{=jlBbJ<9mn|@-}N&DE0;dbFf zONwZkM(Zx6dX}Q_djLeKAenGN)ftvw^s^UzFPdS&j?Lv|bPrDq;7Q#IVFe6MMK}($ zua7UX5m_DJ(BG44NYY7t>(a^WzE{r&Py3-U2f-!iV^~rYq1jG%1%R3{b&{JpLw}6rLWP#q7csMdW`G9 zi*r#vVlS{ay(HDzf973UK;L#FOVJO76PBmf4_BhR%m7y2z!xWzVk5ij=P=YDY+l-w z0ZuY2%9gkM_tx8U>@pD|0#MuPHcZ83{-jl6l87$>8~tO;cXYwu7-@-Hb67DIxj4f- z1FHiszv0HGc4382^seDq@0mpUvy-hF2;?Paob-G1)#Zt8Cx}Bm+g5FxHvy_} z(isVXE8#PnS_Dubm3**<{)1os8AoZfaDQq^U702J`qua{COd8tp7H)~_>}Yb0=p*$ zR)c*Wg0HereaGhb7L)3Wf|WiYb!hCli34|+*JeR3MB-ynHA}KGN}W77!HokmPL@9{ zR|Dm5V`a2rP}x`HZT#uJ_*z{Ifkx9gW2YQv0iXSZfSmX%yZ|1kcr3-^d&&cIrip-R z{eyNkuUC(|n%TNmSfT{^Pk`YuC9`mq9V5qk^NyYDhX)vBCLjq-)BhApMuXF1Hr0ao z-ra#VVjNL0NObIk?H$lChZg>MWOBODS}yGYz8cPULkSwa1M1&a{NX@Xdu8EAv&ZEJ z%O?`AC9`{R<5cHg`<@Lf=Loio|JdQyW@yuGVng!LlkYjseV8698C;s!P*$)fB@y;f z(z8axXL5t^F>|rs1OJMj7Or)wG$;YIHZ0JdI>~F-<#6=j`kQSv1N| zEqP=#EWKw+B7|EgC7y0xD}7rGWvd=}kR&u5S^GGZGbGClBEuSd6=kc4E`7S~2kuCD)@fc!ivGBOw>e#QMI#x4ve2}zS?)2r1Yoj0z_Kp5ggs8zQOxK_MN zg!{G6$Q`={CMZugvo@&TGkrwMuoI6vC*cLV(1`);>_D`uqISz(&s<=~6JUQ0T`rf@ z@Wwg5``--oE|=`|^YEc!lE(W?BAiVa3FtU}ooe7?9!4p(_8+aaH<^^>5S1I`gK$dJ zCc#W6hu3o>voM|+z?*`Uk=SqpuR?^$f(uykgwR>vU{S9INnX^y`uzKcd@>8drlh0c zEY7?wVXcf}Lnm9?pQXjG9nz+eZ=_6P>In>LVaxA5kj4P>df42+*Uem#E6r(|4=nCF zd(!mZBNao@-j;NP$3D7y(w>1Q6%lnXYS#VRM;!8F*&d+il(w*&a=L2#h88kB-E8O% z1(bj>7z8z7XnsckpE*vlu3}qBZuMqT%@T8;k8}jC*4=eQ6dMt4er80-HYbI!$|`PE z#wq8ZAd*5^&IWU6&Geiqq>t77h0Yq(M_7Fg`2tmYb=Ch#w_NC$&AV5vF~@=6rJ8J6 z2ugFT)08*KS4qL=Q?(mBEI5S1_3w9bQ`7e`5~O|~L41>Yc|CN>9&17gYNi~fvEOKd zRX_I9b2H#Bppa29U*V0l+V{MY^4f_VZn21^mW-pcI;S~fZfi%XaRcY63ZS~!K+5db zr4=#=B?m2h{8v$$srAh-Vl#s-&Yw6-&mB{*5PQzm{_{xfShp9(v!L3x_5ep%vxvU5 z>gw{waGmuxydPe!m5Ve3RBr^dX*)389bx4n(F~C0fKae2rvWA43Vx=~2$hI&34&k& zd|0Sx?s|8QlNJ2CS)AWM9JX?|%H~RVuaY+A?@_wyhmb*(-Bo z+5mkGjbT+t-A$}S`Olurf*Y{!3tcr208DB{T|9)0oz_56Xf7s8OC{<k2Cw&XGP=NwBpmclrevW=F{IO*fIWpHxA529EYz3FM zk&*>q1I#Dp@#t5VTvzF4VS|);&2&@*`61Kt@yuKKALvEC@Wu}@I{R5 zJP$N*wgMzLH+Lr)cZ1Ft6zRmfieM;J>S8e)2aY4zVVM&Wjb$(b6GJPm`im|tazoa& zTHW0`Q)Nrxt*)5ZGAE0o-;nVBWjO(hX=U2NmscRU%D4=BbE9m}AZi9dtOs3XhUq1? zWvY}q8pTS)uNtG(WCBJSF6X2Bm$grmy;hTZql-$>-3iL#|Xk-7)I`^r`)f>5~aco2oDGQ zN%{uhb2|Chmo|;vMIW{+cVseM=_%XR=Wi6`ZR#Q~cpdj>PD+d}{@8U{fA{L{7Q!ux zK&?nt&_TGgFeSb`slDKjcd^2P^{ZN}I7~2x%k{S%{*(Cwbu865chYkxWk0;blWe?M zPacsei3o`PfAW-BM@H?hj1<;5jNTk6UFNTRp_q|ig!YObz|{J_Cgalo&YC?3+8Fu%8|7!+cL1H19%(8-ub zw>wv;q5m`~gp)u0d^-RZ>Z@xVW+`US((-z97UESa)hQbf__Tlv?%b}4tP16lZ^7MDh@oY>Ql7ol5OZ&OeGEFlBr$Hw z(+%TGuey?;dCsRQ<3tO(opQfA3( z1j?Sg(omD6w!j-Lr7TZX02l>~WnPMKNgttfDjYurS1r%-Tao;s{XGlREBqIx$Bf&3 zr8L6lu0(eeV0(K;F0xmpos2eaJii=;wi_T}fEiX;RenC6jLEBCjuuq~g9wlzR_m)Z zmR)zfLf<4{Gy^vWSGO1_kVE~u39<%Os?*wjme3(sb(aHL(*X~B-tTi9vJ9@WZsU#; zl629sL#_%{_qft%{SrQEpT{fXrmaF7IGtN{1(b^$TL6rjxw+L`0z82`1Y$?92MPOD zPbG>f?I6NVV$LUd#`cPGl)^m+LkeVW`aAx!ghChfHW5sG;ZHcH)PLjsa`z+AVPyri zf+R3x927^;Z2P61wc4We5itaBayrpBmWhscZzX+DZeL=cyrN=f||O%;>ZNibSMaN7S{oAZp@iV!hixUibdvR8yzN2w-BWc`%fW z8kdxYb6V8OQ=Z7hy!xckd9E^3r;39lm)+r?cWUnF%@;ZU5%%OY7Zd5~zRkf=097SU z-$TsSVx$|TP6D^`O#)k{Iqr+>H7Sw~Ct47ueGy~i2jJ_!HvWo7eWMjGf;~1Eu+oFt zWl^=1G@7zWJ58+H{mIy11#@XLzFEgVcNOe6@HtN0jW)wudoJ>9;$SUA{ToRJipLG7HxKg_YA2=LazC23BvnKmEk9&Qco@jCTIh>B%&g?rGQq~NC6ssbn zJ@ahlW(mHj;|(rJy>_W5^b**ZKz@5iz<(Y3Y>fndCB*Xxt;~T$)Ah2h@gwy|s#c_8 zf-P6B>g)E<arVmA)ctg>bMC_TbD<0<)zFQ=Y6K_XMwWwOuX`VOL4og-0&Xemg1^ zE^Es8Qnd(?@d#R_sVv?F|F@n;&ErGsG%DJ?WJ}SiFx4=gS_9C5n1uaoo8GcR_ZRWT zp<*1HJM(De)PSU((t>Sroy5axHJqh4+A9;%J#z1zIsoB^cPk=FF4WWO(m$E9Jw(3sFH)R_o)%V3`5w~xUyhPA0hrxq7|d`r4uFScqy|0*^==dG5SzLPEwd4 z7SiBWPYGEG8Ci3l*3)tO1DM@na}+<9lL-4irhlbFQO9XA9She0DkeG z#SNf&orF$LVSNZ$5|KTdVTvHz&dUf`7|kSWcuFsgDvjw9#v}U8{skM^Pb)d(9J5;{ zEe~Gp4gEKYN@!|E)vs~oCzA7CE|2LW!Q1z(tz;K4jho@yF}{|g(eJj|m# zCDU-+O8z*w&;j#@JibS_qqbcq`Y-!8zBjH)z{0) z1YH8N*#wMC4vZ*Nj}2GcJ4Bbd1x5QKgsQ?0eO zIq5+@hxXZ^?v6p%D94$z5rN&ozah+90zup<$Sbj2kr%b%V@|S#tEvsphLzv6`%)?G zPXRk8=(?KY5f1X=_%|V|s&~{58szU7<)>bDFDnAMiI@ZMF`n|H`+_NMV4KmM-G~`P zxtLVnGE+QTd0ArOz*tl# zZq3FXnf4lr*fYpXIf@bJcK`Y%4a0fTK-&(8`Whv-b(S3r68pmq@pWb30l(T*~7LKjTj@ zfP|I`bX^}c8TaVkowYjUIj>umaDe}q*z(9Jb;4@A_bIX#ulRa~QH>6{^S!M!ehC?4CQ)sZQ*gM5` zciynIzf3q2OX4WwVobqtrF_O&7#>s}39{x`Q$ryR9BFuT=Mi8MmqzMn;?U;gq#_s4 z$CDDbiwMRh{D~c&v}4b!!;`3OA1#M<;oDP|kJa%#ejeSbTFGL`S2{{Bof6%QLvGPP z`Z5QfR~9(1?1WGLkBs6)I(ZZ~X`HCpXd@ShQ~r?lzh$?I=bgu=pHJsS7?j zMyAn4hn6(e^XUyZ*Cx~KXH#NhJ{L*#RF(pKWc|&RXu*xo?v{?C3YPyR+K)GXAZ~67 z`e0OE2w`1g{o;^BL!sOC8fn8H)D<)UDE%ft3WLHklYP=n<`G=bW&gJr3I%;tmO)l~ z|4`!BKq4N~{-|OU%X=#t)Fnn*d7s9RwYXU4*MWpynvN#p9FW;S`Z5q zHhqi&t*P;Yf&9G*fe{bROmVOw9xswsg)fK{zWa2aOse&{$5Y2t2>aSEP2q`gsXA7$ zlbcE~Qn!Tyvzk%8`uuu*YkRPH>r@UTCoy3+1OvZV2=eM@=`F8AraANv2B+_O{BvY3 zSLxW!ZKKMn=5+YqpplXb>s&Q|+p$KfUrw%tI9Fh{fJ^OGsFy@JBtrx5OHY-v(bX8joULaKJo30(`CqT~wTDrG)(4cQBn#-^#=Oj88HIU;?VpgR>JvXd!h7VcM z9%T*^#LVoZaj4jzWs-l8@-G$U*{B8cyj&9JfZwXCFDd#C3=I=4Q%Sb2fVvrkp!RH( zGY(hP=s~Kzwr9@%5YC3Dp zbMm_P`#R-c2Y6P*qdUKyrt_$vh^~YbXlIvA3X@Q>_XmM^G}`ZrT`UR*B_F$(91z2^ zpSdo}kS9uhw+#^1=1;+34a+nB;F{~dAQZcDhAR%f;`MiqX+U+js<8y*VFR{Zs5(EY9A{R0R(!GYo z;)hW@r6n=A3wV3SDlO!Cc=QhIY=EuI5+LDZ9#SYGUd{SKCibOwoR&B%wDfx+1r>ac zHJajzxCCa+B^*b`Zq<&ea+U`S$U%Q2S4a6l04Nq4mXA2^J;=72x~NcHj*cs-N>Mh1 zVPn{Pg8d5K!!f?4VMyc0&TR2xUJFf@=^qC!w)bveB%6Ux&yKn)PJ8_}lJv*~v_h>5 z;q<-eU#Y7W$6et=qgxuiST>UBo>f_hT0tG_PSk{GbUiq<7oRIwezKNC6TUL3?@*dhCye^fOkCyge09P>x+KB+%Z5*pr;zjx27QElh@_@tH2M zm=9e5k!9j&dx|4R+m zP)#=*$v`db^UiI2q$y6BADPnE+LKq*cm1H&X$i9agYaZx5yXCRKy^zqy^|bx%A}W( z0nPnOIagGrGt=l{ibGRu_n*}k6=AWBCH;Do&m6Pov0z#Wg456j>LvhEe(TJJlm$q= z=Gicx8F-XkpUM|F%mo2!(NES>h^7kiON)@4iEo0$eX)shx#+%AD6L1})M@^mRMiKg zY&vW;*}#W7%fgw;{JoocH$+l)TzFgqbG=a3B@*?mT7FrG&*ci9kf4mu4#dK=Booqi z%>=87qr#d5lT;JqGS^DU=Nfw$Ssg3HV4x$DZb4W7uG#ObyKYn(9U8lyXSnQly+)aM zLAI3UfHl)#czp-0;}}}p-6T}vkMd#_k93jdWP*Atp&;?KiwOh-Z@JvWARQRw8|9sQ zUd?~wSr+*z~=VqX=UqOgAVBNd_p&apN0~xadkzg^_d#z?{E!u z4{!_C$tvWI4e_OZV_7;|l<6!>)gpWH?M`6=G^IeCplA#&Ft+B$wcbV1W(x8!!4P`M zxgzm zz2uR0E&YFTCoiby4Xh4>pAbF~z7h4vI*Oc)y~bbAx%*AV_*EmtW%j005U;}gfYk~J z@Le)Kqu9c#9=yzA9pbJxJ0p#UmuAD`_mhBm<9G)7!0p1ysfLqEVt`DI3qMM?#dNSb z8H7d;=U8};2Qy%h-ju#@pemQyY9rs?Ifu~M6%(5pO$sEjG2+iH=!0hlH`gimd~GwV z(9<2PNJ5>`+b&}^t})fDF<5?ouTW|Q;3Se3>9+~CXD5dL2-Zz9AYN4JdTl6=p+3CzqUbg6X{`Gvi5T-= z^2Ztcs1)|pcWcZf*3@5Y?%+>Q8a0usN|87NXpd0ROEb6sA0I{Fu;p7`c5g~QY|nka zYJE*#dsZoJc%F9I4Mkfa-~L;QeuVl{M?FpOm|q{;L-7 zJlOGdYDY2+J=}%z! zeKVs&zFNn<>LU`wc3IMe8~04OqSP$cqr}n|9cHOn3u_F>Lu(mrN~)kxf2C>z#6X1r}%p`7Or+yFHDZoJWm#I_Q8py z$u=7k-@?8}iX%3ZaSO!KWV8!>cwHq0+~tc_)eM)Lr-=5HQs;ZL3Ou9j{iaYfAiDD= zC=x7@o|6XU#-z(eZ?BQ)3x+D<1-U8Loo;6;%)c&gdh+#wJ(vSH2%t01TmGcV?lz!q zuG@^)x%xjY(yeMv>?q}m@R2Qp@tiUN1E05kH>c-tNp%61M_@KKgLl%UWxJl5bDkf1 z;LcOVkC@2j`7ZQ@6@5mwo|Apj7PfRzxcC{l^V>2f7{3hs=#bOq;FZv*K4d*_wy5%lvKC zxu|1BQ5L$>IkihdlG$G2`2l1*Ehs70IDH5c5?P3=6Z)pMLW0z>UhLb@F+MEZg#nTp z;rr*2=>Pb(u%F@S6w|I`m5=8QhzbSCn4DqXfwv2SB6k1diKw}dX+<%|RgEkY`t&o( zV=tG(A2H-+ky@dwzQj`0w+(#=LscQMy<7`HF4`t*hJ+AB0U&0gP)qOy3w)EP@#mj* zsy!=!zAE3Qe+j>QImh&P(`Qd=6Fj`&wR33JXHU;YUH$>=X5M|^4)OCO!{Pul#~~~| zDR5-m7OmtGlfjVW#Dc~~Yg7_J_62`xMfK&|=H!(?qOu?<(Qtbzx^6xAh6?|0 zY&?buwns41{m^?*xvyXTeLLqIbcim1Ow1T~>6(Q-O{|&^vxRiL49EEx;eaK~Z*8V< z6zh4Te1B)T4k5=j^WFytmyHE+K6PU3XCbydA)g@-1gWE0W;8J+xVmX&&Q3|k??#JY zWD@^jDXE0;df~_M{>x^=4;z)p!uoLc+vxpMz{|KSOKq4k{ILQP?i-Kl=0Mr zsR+B2`bXb11my<@&{Mdv`dyZuN+a0r8vaTIse@^c^b~S3gocmvfpU*wht}x9Vv6at zZ-;443bU6h&n!l}>U@*xl6GP>ujh~2oj8%Zc*yFQ*Uz2}P4c6X>Gtk*rmrVuUCYM@F6*A0JdtZAC zNRxCKjIt})J4~y-}H7cw_(nWlmTu0N#-e`~hGbGCDm@|69lq>Kj0?n0{+u zQD$S_vcJ~6$!x3V`y?YQfZ}v7*fbcC1+`nnowtY5w(d=_;(XZv=D4#~nzoStcztuc_ zTuSvslI6AL2K*jRZFe(hEPnk8tpZM4=Lp>2eaN2lR?Fp?vuI_4HBqL+4LU_axv#T9 z`CWorF~EI=GOZ4~6XW{#Cl&#T{NVv4&U>b5=~s8^K=I^Kvwu1;x1P55R_N9HjK=zq zQe3+VDS@ROAyC$;LCHSGY^LOPu1H>lu?ly^Ql4Cov8M_7BP>q8ymd&31()HS6OhNl z<@VPAR+{oOI1}7gm8L(hXNma4)=`c0>$P!V4p!V=X{%$!YlW7|Ywm@y$cGMtL2>7h ze*3?Uh*i3Yio*me#vH?*2<{J z4p;CKzz{8cH?p2aAJee&F1*SZVA*K)!E25v?#rjs;M{YZ;$;axAfYnvY-5EjexXat ziT0S|-U$s>;lm5qidk3elDTZlpe!I3!@lJyf?AVFzM0(`_n#C+fS& zM)Wv^WdhWaUL=p?FS!XcMuf{T`iWkhonn#W9CEPdr=v2{I5Kyk7b@1vC5B4Y zorG_09#N+}kC{*CNUJkq0cgy>p6j>Q;k7GkJ;!*}CRIa5c+iaNtzFJxevs}VtHRW6 z(mg>+VABQh?t+$+)JlaB*BA5rgc%{soZgX*)G;+z9e$PmJKo8sYUOY5Nj2&o#Zt14 zCI}*9{JT5F%0SH()bqVUN@D6C>WNJ8JqA)Uvmh(B5)UQO)>8zZ63O&woL-G-KQrF! zVTLeo-lTl-#b-_`Ot`7asw56D^EXcxZxq&^+7%DBMWcExZDJO?cke-^OD-R8Z#ZLA zcFI9Y^UfFN>{T_v4k>mPtE zM^E553KqWZrK2!iO)1X3#U7pgS4miWVN$buITqeM>I&!4k#AjVraPNd+b?ynHgHWr zR2ry+1}Sc4`U@RwBGD$Z3 zMVB(sLAsI4qThfjaQZC`$2k!0QtTg2CP++GU}MCwL^qB+xYM3KHbWh2UvlpNWfPGA zBi^fuEwT4bvf6e6(p}>P(5fCiUv#&s>$^OB=;VKGNt3aBfipL?x3YtlYYHIL>jh^_fT%m1-QG`*!F$yUh=jU{eO-2wM`R)K5vCL+R4*M8 z+E56Te6jRBJewB~V{@&ub_mG5JDnOA@=|7SK9HnotC*XxPqDzezftYgUE$_StcV1A zKR+1ao9}9&=Yt}hmO9@4ySKw=)?2LKwyeh!Ho7<`jj)Zt5@Dx>*U&jJR^Dg7=K_At z3ee2I+s~Rg#2v;fSH5NE&QaDA>j`LQl8s@Zg6fOF;g{mhLFjU3%saSJE>Lp))92wv zVqhw;GKK5njXbBzkC(|macjshX@dF8Md*7Zu1*Hoa49q&@|^_hcqKoF3(Uj7l@sum zuAaj`!V|?aEZLW2RPiiFG8AQ&o5X$evy!f6X%DTATmBNs+rzSMf=svD=Q6M|D zxk;F)m-_Z9M=e~_@0zv3NnN}y!}o~`hb3U)H73HW9xr#ax(0Pe*>w}zH?F;F2CsNN zStW(*%`|djsI4!f!r(?{%mhPQ=IAtME(^!wGInB^R@*&##=>WJ@BAP8-$p;w^m6Vj zr(o}}5UcJFlhM<1QBg!g$Iptr?jEGJ&m$T-MWJMJKHC?vmINbkuEvGew?vvhdU2b3 zf!}Po)MGA&IKG3M83;vH_&6hr-f${HLxW4A(oTE{&%D@36H^Dg1-6@3PXvp9R~*EcuYxeJSc9FUH(%HR-Vz9vxzipj?l z_P0?7z8GY>7IA3#k>hGmA{S=|G?H`1T_pzvsv0Q#thDP&8(4|Q6dj660WfualG#S%YVIjTS#^wi|T zgLKn;&6kKIX!5`ZkiAnR`c3v>D_e0nGcUk!Ie2!#p5mA-f!h&yd^YWNd2i&*YtP*{ zbAgCcQ+9iOS6@~8P-}`JxZ2|eah#rkeeF|jD_J8OJWIRvImgyTMhi#(FoqkOf}^uR znV)SvA-*Ly#~YkWAO@J>O?MN-WJV1k=bZqBe{*au3nYs2Ffb3Nh>JfAmdfc%A23{p z2&Zn_@=tq`jzU)oLzIYI%k&Fz&W+l|+{h!%3)nSl-#+`e*1nnaxBWp1%MC(_yjbCb z=#!tKUVRsVdGqu~$iN;p7YKYY5tIllf53#yl*?9=*wng>>xH?H+vr>^0va2YffrT7Zrt<9Kp#~+mRW`$es-Aj+6N`>g z(rR;j0U^5YIbeU=(#m|x?*)NB-q|2u#%M_Gt~BI#1q+T@E>79Sewy?a9rdy6(X_;= zl`T2}_Ltf9vJsent=`62J{t^cE9opbXBu_Q|L`vPz$=o2Cj_3me~ir4i#xRycr)-e20iyj71;lq-wQ+j_kUvCDcPwj8LzPeq{3P(j`LXF2<~=ER5$!5 z`i@4tB?Xj>+Fm3}xNx;Dwydf1%#K)g)p(;(--~@1Fx%c5PCYT*D9Fe$CbI%gwm}92 zC&hclsYmi8=mpz4d52CTE+@qBrl>EU|dzPAH=Uyyl{s{e<;9(r}2Y1_k4) zucr0)R;KH0En4+-YkN#8^pe^kjyYHcF(B15>84~mmDhD%bu_FMNlDyS}T zr#Qg`rkTNe%=N)*5^YIU-t4Qd3F1vF<;WNvh+V>Y6b&WA^75qxE3G`4rY5P}el`ykk=IKh|k(-Z?lE;Ih|8u$Rb82Ip~PYT1( zAFUuZ6k_~H-fGvCnZ+WuWjk6Bt(z}7rst=Q<4ZOO0^}uOiXGKnS>~yB7@>YI&*xoq zND!RpJ+B@_ajdIk|Dy0;3z}hbWzq}9tKN6@t|kiP;s4KzqPcA7h{~_`;K|PZ8~b4$ zIfpEzXN4}c>9W9H?Y0KM3|vMR;*EcPLQ8a9#kx!Hno6?)FwFP+NW3H50Ju-ayi%!iv-g|DbDWDa$?Eosz2S9%B50hbvRIYy)*$= zqyCv%Bn=BgLXmO0#cJwL&Cp_vWdw5S1fY}dNIzG^U``CoaYB%1r{M<)WOk7BHAE(H zOpQAt0sqVWLtZfq1qtd}j6XHOvpX0!=X4fb(Bl-?!|ecga7vp#TAaNz{Tl}hE~p3w zUE(r>VP0@}`A0}$zZ_`!Rh;|B6Z4TnaO6C)nh&)@1Tdqz%8 zK8zT{{X@Az~gme<`wk1>7lPq^+BZ8%+aDq$}c+J>06(fF7r2I0T? z{=Tn&gs^(p>>&cSq{=`-Q)+&l&0K6O z$%)S|ilcgFI!_%GEJ%hZ@r&Bax4W9s-=VeT#r=AlD!D2&?$=UL9o)NaNA8eH^)&?lK16Elk4fxD0)u- zz#rJ+##4l^1OP&t_K`etd=w12J$@`JR(b2jhKKjTudz*0qpA~xCgTE>h2_LP9<{_N z76Tl;J*ziv8F|`rgzyyWUIkinKP>l##2L9;dKX=^s`l)B-a|ZLaslOAuKyd>Y!PVs z>}9y#slxVYm#aGk64FDrBwEKO! z=++43r?tgm!{_Ux>JsD=XN<~mNPL4Ck{S(YQJUY~)K8_N zxY&;}l!JLmlAk6(6zPu}12*7TvTqF%tKOz*&`)z| zdrHU_qWf@H`GOvZbw(4DbO86>-3x96F;=OY6Lbn~3y6V+sbk7w3c~tj*k5OeXW9i$ z{`VCcO!%7>AEIi=%j^e(!A;%0nry3lz zz=m8#J-jSX2u((_AL2TnD&YIupuW9mQIch2KY0dS%eO;f3(4ij^Y5GQT0$FpL)M`A z4fgUw_O|)5j$p;A--85x%>^-I#(T@~GVE)bm@lL`@mX96%88Fl#W`;hKxBBv{s(1i zPykLb_0t-lOk*XLlIb7`aa32aGw!TNsskXVQ9dB^@m&>y%K^sOFAF26J8(X|y$+*; zWBL;dA>MSiRNTgq{e$Udt9WAdQacu+DALoE?mh;S&9I#~hyL4RK1b=#*xV&#hV@QP zg%w)71(oyXnJ;RE>8=~_Y(s4901P9p9#(#O(E1(;hNx@-cX_zt8ImY6RPb5cdP}%T1P=iPc{B+^Vy}b@F}-UKLNL!|QK{p%vOt>3@_4&A@AZF9hd>U zL~hPL9t$~chycGRjT@BRj)IU`w}S5aSR{?CTvGV4u>J?SJUw!_=&1%L9!L{<=M&;# zLk={~-bKb(*HRM^PNpHX9f#I?4&2MHg*LU{jOBQOW{)-Ht;tx|b{Q!chZHJKMx0=s zBTwJ7`}}RmQoF`svV%3AcCH1Y_myy3dcZ@ySpp~h!oBq7O2K;C z_IUOCDpx^luf>Y494-;$E&A=%9q3#rU1kbQvgqJ35y{qIit%GVBp^mY@jjBJ%^ZQ8 zTkr5;r>wGdu*^^XNQYW1^K}yqig4>Wzd03OB8T1B{Eqo;J#9miFdHQ6%57W^>!8D# z3=|Qqbm`f>Ng!I9!t9Ke#gx*)W-zxcI8xL?+TH>^gZk(kF6>ga|JpmG;HmNm?R{61h;4qq=ddaf=jg=C`>kwVR> zF!~T26NidS^gQyv2T)d`c!kUXt7o12Sxi>hmFnchYDu{xQR$%&Ch}{oYjMhg3@_xKA3r$ zYY97(Su}+%gRV7?3@KQmnkm|ox89nM$KB&fXNy+R;$A@HwOpo1U*CC&+Ph##$=GE1 z#iA2@oMUkVFR)cbTd7LS{PNNMSP!27hw3*yJJxaf23X$1VJ*`b@!~$cTlLVZ{9+o) zA@3!McWEQY$Lo)HCigpi^ltW~dvru(1b!zIs>PrP*HRtZ9{FfyJ@qfhRR1q1NEzO_ zeD9OCOJR}k?69rCyd#pIu+eFz4DqGTI_TKj=3|UT_m##GdgVEhGS%<;4tXcCAc>Qt z)H%k|3(YvrnfvVi38^F2t5YmzNo`t@)G@OXhRuBVRSY<8SbDd#*yX9$Y=(2*0VxI$B|z za_n|SexGbUzBtC2pYEg&z}23F5pc}eTM(+kJ1o%j{{FYNIE##lD<4@Va_jLWXX{O| zO<^rvo{V9^R9Ipy;`ei+0> zefMd)x!7*YNH@}I>G7N6t2d1F@dA3mB)o|Ir{3_@;a^jkyM}%6cyH)O`Wo+I@FYd* z=bWT6I1cVID6CTSS#^St%YeP{KiSc|>aGur-C2YY>8UYD=0o6@6R4sDY|2i9e{Cjz zxsY7JA&r=bvxuvtj;{*@zwK~87EKw%9)1SDMuh?pmC9+NO+Ag#Ob&c}jFZ!#x zHg=uyA`kfyZNC4y>3xB@3!UP!YJFKgA_>%z&Wd{l%E>>O$x;kJ$*`&6cx}n4EiLKS z6z(8v(XK8(WAi=cgVZuv1dbFO`8^+P4+>w@7Ln}o@)h*Z!(9d4L}#ZY_wwfLujEPf zkKs}4*IhZv1D?NiTbQ>i5Cnfgu^5laKvK@zw-@AA|EMlqcwW;RmWxMP`OM2vpyZ9P z)ZQMQR{Q+^T|wylGDF~n8O~e-^?3w2^yJum`hbwM z&_5ZHgyI!YTdkF zjV00O9R_#{t%#M+P7LqjII>9IXjJRBj+&UhqHB2?rc^YOa%w$z z4bugMM@5Vgqp*x>P@K0+bTmW!%CoJ)1AiM_ZSB6FAc^&ZLWUlb)xmNDrEq&0@;JEC z^H%t*C6N_0vO0X`XQ_Kg*Jp#3OVO-t@sfLm^yWLR5x^RgJJ zQA9A4nY2y-!|I7rM%$7#iL#2OzDCj4*W?it40q;fDk^7vjQKUGZM7Rm9rr^yO_Q@! z7$=mapkX5oM1ZF7j6*U{)_k64)a^4o!4|1hBouC*VQt(1F26%DPd{IUC#^@>$UU6L z$I@_sMLj4~QQqQoBWwQ@zbQ(hjW!9JXG1MVCxn40E{JG?V2bQ6p(W7`PiCYa5iH9O zNdUa4dc|bGIgUbFR~`li8VJHCmlR2nlJX3OQzjkmtE=Thg+;!lt{a-IHLWVE*w^)J zwl;AT|Lng%QJ6pT($^(JlNXk?Tl;<-0u+%bY8+0YKIm~osKbP;tW`(66ycR8{%}7} z7>oV`0&EC-E{@f27)%pQ@GJB7gGiN+I#NV<@#5+9(%ycYr17-8Wd1|mXuba;5%S{W z=@N36S)Hw1_9BQXu8wV*LTTa-fz>;Z^Yt+aORf+?VhA5QY%HwptwTeC z#J`kLFruEx+Nw#%$yA2{PjR>N<7K3>(uMno6{du_x*2jCfS%Tx-Ubx|i#WTzabU&iNr=6Aw1Z;oN;82D^0hP600pfRX#*`hNJWbc$x490dE2ZA4 za|BWhH|vfAm9_EV2Q%;C{wUU^7yKGWFOjOYhan~|x%mjxQ>C&?TNI`W74hibw`}w+ zW)sZpfa_6@YN-x@)%%kdYZ>S7E9nX}SmE7p^nXQGPt)SBHgb&?VZ{NVj^)_ixUMR) z$U>^9Cr8px#LeS*#e9#P&QgYPoTnEnMC5naM>x4o+Wt$;%C4`Z z(k#Q^F-d-iWGl7XF*=xUp;n95zJ{aj+w!(Q%5B2~45T(AK6{jO#q~4%S|STPk;BP}sDQ7^a?&R)!I1wV)ED?wdsKE+Ex&46rr+ z!{D^JGU;Pzd#`@>@*hq(q-YvqSQFM5-_y!p$&$r(P9W+lN2UnpkpF1}uhDRV1wF*gshEEEo*v9L+AN@`LGvgH_JBO-X~yciY+6jq0DMqQVX- z#y9*YP{lc0<73B>nBU}1mKkuFH?uv?8qo*R1KzIKVw>|jAdX83d7vI}`ntWWvmVKb zgixId6^pSECY>}r$mbDTdp}0LyE<6_*p=8|sT#G0JQwdD6>P%miaF{IO));gvr!{9 zuR7rWc%ZE3u&kyx17 z_%~^@q*$CYHrD;ogfPiM84YCvNS9BxlZsveU7czJs5zwjtv$IDPQD-s_mrymO~8AW zdQhMJ2Pr^jz*BP=0-}zGcdN-IU5OmXIWYgTuTrO5!wc}?)2iH_$tk0xVl=({V-bu2 zH;6PlLT$XD!RPmcVdsg1fe=&^0fym*k2795ZxF-L|EST6pmC+V+QOBM-U zPgwS;_}aSJs2F-~BKSxC@uF@|cz$JMM$nWp5G51MoyO$2VAsOI+Nyit)7)GX1}F`X z`8T95mCGCK>O}tQV{W(w)6sc@J9M96Vl%^DsUsfpS?`|4&Z3gj1>Fx5)mo=2bUR|4f~r z9s7Yp`W%Mb_lx3NGRPASI+V@?T*q=S_|F8#n6}@4{tC!^wJe-nPCs;5UKf4#%ap1l zjf3%N%Yu=~CzT9CE`+G8L-jklEl2!ZVU4|LrjNSOIB?NYecW>z?X?Q{NsnJ5!C@EM zPbDx-xOcqtJLrzhF!w^D{gx`8(SO=6gb*UykmVWmUF zEy^5>Qd{HR89vGW202B&W`bU@cZt9!J<u9Lck)o4T4dw`6 zUeuu8k$x4Er|>94QQjws>DbJ&Xd-Tk?^B+b%JS`;JYaIz*Fu53L6tM|+I{ z>S{7!Ql+|ndWwbr_quA?S14}vV&#P~E%gbl( z!Zml|^t>jczzTVGAMx-gO3B&*-IsoE%6+vA@Tamrp&P1&E;mAeoD<_oe zUKcanrjjFNg`Yg&JgLcyH-8aNbll-{g!5{zsr>+Llasd9-!eL5CR}g$9=n`&dwC*R zX#Y4KYX;gwhV}?ea37Mn?Y~vzeFavUhK`(K3EV+D#%R<>uvo=48 z*akgG8lj{lVtWO{Z8-Q3@4jndu7bB|-SaE>jiww8xW8bF2Mv%_u0-yCBxpz^cEg);xBkG zGno@9{Hc^1t$GnK0mM95M%@X?%5h>eKjBAi*)7`g|A`OqhL}*Osuc6lt)9j!4|#Ro@}t@HlU3`(we>NX0^`1M~(FI z?eaK89HeU}DO*1>K!L5Xx795;YpfVO-Mg00lLb=YhHwoUONa+07l&%RtLIN#a|u=)3|+ zo&97@Y%g1eOvha3ea-d^Y4cUbz(5%Zd?s#z{0S>)2b{uBWtmsIV}~vQ3#J_!Q%27B zza#@+O-#qaU3Hi+AzqMW#f{^!@_!+xgka*h2O?;+ZD2!8ogLS1VWWc8)1b&mdaT_A zAzsB_tY|BJJdai=Dyl0+`@3*@1h0Rpu~0elASpFPCJoxA*B{(7rgD3rx)X|C!02W} zOc2G!&C{g)2zhvrdxdzciq=Pd0RI;xLewMHYDBa5v%yr)^V+9%HbAqLyQ+&@SLBT%1-X`Ro7iLp1GS5@K_0wM%!q5e4VFMs^f;X4MN*)Y)12xwg~LA z(s;&VxQ5<7Aufskg@LCrA)_tttA6XzfMZwn)v<|Mh)arIw1cS#{u_cj@7j=D86I9B49PT0t;|O{ zVu{N(zpwy&r-$ajyc|vB%rpVTbQHewS_nXY`#m|n0_8=C8@1)n zP`dDwrnj>Fi7!w1&3P4Ze%Sc0fQR(0S*Ux>kT870b&v{J-0SedpJ z1|j=S^B(z|*zV_sLZVv>K<0=;Q$CCiILIq$2FW|BD5>6@L8 zri>u08B-!sZMIDelpwHR45gOJ0*^qW>N_4D*9L4dfMrJw0o| zf2ZF#%^7`M{+ESEZQ#@6gZ(y(?AG$cbv{ThFL{6f8FCDDa6Om5vn)`|5lQi>ER!IeApHigtA%DjZ|7x)6(P|nkgL^~ z;5h2fan$^G%b&Yr*Pn_nrHPw0x-I{ z`qw+f1QaQr7JURZ5Sn)FB=thN1At1uUZZLhf!;;dzN08~X7gP;nB-!^a|&M=B9z|H zn~^w!GN0FTvx0oi!B{$>#$l{2>V<>TczWWxw@#x(6M5+Phn4`=CZby|2tqrL@4Y%- z><*Tc;n0+6|CQKFnL}Q2<`Y}a$EZ6X67!;L{4IXtkFWBz_oGiEYhgai9YVTb z6pORO*pAIu!M&?#Fmuxp$um^1^cu!_$XMdbp+(8FSRg_{wsgJ%N?h}@087!X_p#GJ zOrk1JlovJH(<-93271PkQHx>}f+NKr21b4uVBQcz=4JwMf$`#Tg6-VS2YRXsRJ zlfTc0q)kHMXSrJK;rb(RMD*oFUSkXEx#hGDDMR6FtA}PIfkRsVG9eHa*l=14kS-Z2VR6H=;cq zxBP@XhXsmmgEcl+t3h5e%7CB7$dBL$EjwNzFqyoSJL;n@KT++2N|<4+f{EPkxX5gP zZNxo)tewEx1|rK#muPL_xx{bYO5m*-kN(yRLo-OtbkmN>te@`=N3&|LK`L7g!&oQ^oCV>ZR;X= zQlP!qV?j6K^p0CRm?e4Hthg)WHSK+4R;OC2)@T50O03jI+Eu;>a$lC*Z+tSnh9NX2 zz;jQILXdVvAASFv+fR<*CDESM?(v}kPykUQpa~-8c#oW`LTMuB%LrY08IHWL_gUgB zLpP#bP*10<$g8S4mfa}LC>IeFI_V4!tQ(@~9!=?iO?TqS1rNkV$(1U=tIXgseLKFUnThx(J+T?|LAjkPyA_LHQ} zCRETMZ1x^);`%VF-rZL#wLlV$B7pn_-}T+X`s=IR0JE2+DUSGbuo%rDa4`5;--N6v zMb)Z)nDUtfIwr7r73-HL*lB8S`vWgCat0KE!I=L;|hS|FhP<~OUx5W&)^ zGKzM+2b)JHXRIrkN6jDw+{zvTW|8C-Vb=iMlEH>!NFjWq3jr-c72&a7@@ntH$rBq( z_PRb+P$zdg!fifNlqX7yeB(gQTZ8OUH?)**)d;el2=C+*@%sX*URAlwZDI&Z?w!Ch01u>g!Sax&fqU=JM zVioYb((p~*FsA{^FIimISBSz5>nL$JP||QyB%nQzw-BX-G!HlF>4~+n zWovzW2NmyShH-V^f!Gg7tJObsw2Qojk&~X1$mOV)G?II9zMfFY;0~aG^{(WF*n`auND8*sD0~uAC;dwqc_3Fe`jX%R z7ou)ni6INnB){v9gt#VBo#!1{FbG_oVzn;_NfRAeuxh#hlXGY8i@HM~ zJel8sVaeBwFs6ywcn{%w5zpU5@asF;tl>_w1*i1s)kAfXaAWJ5`h&X zAA2|P&^g<+)~3m(^Fu)t0hUpLA(_WeIF|EOQa;S0pwB5Ul8E?U>xgBjSK*JW*E`-( z{PB3jsGMw)=|~Z@GYLF(^J1T?h@2Wk$60Sv0Pw}k;_B2)0ERMcg!r&pSzLbcH8-N> zl@Uy9@_`79FKzT4m=wOcZ;sTJel~cbf$%gFW*CmF9dJJ(F(-S(V8u`& zDQT~N2t=Nq8XmtJvyo?ZJ5WlLBVC40+<|oi9$cmHDN{^xGtV~6-hJ*W#k`9Y%;pp@$7pC_d9&o4VhQh^3PMTSf!Gr6I z=}jnW7@|ad?n#G|8>Dq?;w-gYs{Ni?-8_H+^I!2`P33FKaFCF9dMco=mg0pPz7!_Yr&?%^+>Ju3p zzG1f=H=pi*{DRs%&garv+}bal!}Tj4qSUR_Trn~xqX)n`bm=;YGq-V&(nr`_%+|j83oPJDXODP-R^hEVdL~lg2K;}rSH7v>6#Ql3XgGsPs`TW zhM@z~l!1H5*pvi1Tm6g8&5|fI+hNS#1_rr2Adq4D&v`qh%Ols&NkTeZ3c>^zH)SBe znPu@R()PexmJ_d11dPRv9l3hrsL>)2-`8?cTEE`!R<4Q z8USrp+N|buM*2!!#vV!q0f4ISAePe#)uDZ$(DH5?$TjXxWoVbtZjKP_MDWPkT8AN% z-vH375tAINgketM1>8WkiE#0fQw27CBhYKQw) z3wEzfp8Co9SJ88U!bCCJO+q8K^gxmH{?^uJTmpY+AJ`_hZ8_K=!=w4F- ztw*A8S_P_2Gnb%+sTG&s5l#eC@%iT{y(08?crRID zc-D-iB00n{4?>B~26g{@ASDhVgrKljWXo|?$Ca(oGAMKv>&V65+LiGUFnBNqp2m6O zY|YGY-=P-#?LE2I{MQ{iDU{)}<|&L3QbVo@2;Tc*vi}B1D*ngK291wc0LQCelCei% zgrBz4R1RSdVWXTDcmOrD9VLid`|@gbK0ABaxBThQ1IN{qZ7z+4eZ|crbnDD4LS9g~g7V24*cpy<#>2pIsC}aEv z`*NfaBKlfk=})rEoAN>;QAG#h2Q;t#&_nt%4b1yGnCo@Bqtrf?q$}3%3uN_=TzBB= zZ%Amtmna`*Oo|mS$9B2PT&g{;&iF)P)v;D@SY9uomR3~$mW<1@UL9A_FduZe3%l#F zI#4TKIToS3p$#<@9TYbF;{O|~IZsV>sADxQMH&}~5X2^>kQ}=TZiDh@O6pb$tqp8O zZW2nGI=GW-sw2H47l7US!0=j@^Q5Z~2kTGvCWAi9Z*JVMMD*YgWDah7`iwnQdo%BZ zYAS2pH~UHLEJTAdgpQY^d3OC)8RU}+=&5HvO*wew=zdH0{iHDO)3J{_NfmlAOvs;7 zXfZ$SIC7hEJ{vpcFqX8qTzRq%YVe$J z_17)t*qP~zM4)Qm4Gt63N6ea(!+U7!}&u zlP_0me~wTUK!qq1ZKn`zb=U>Lj)ky8P>S&8u}P-nBycc3Op-GzFF(61GV4a-GZMLY zHF%FihkDmlf_hqxulmtZLBQ+^YztK?i@V%`fv3&{W6qx3P(C1v6y9NbYQH$z-UZM_4y zLZTyh8a=dYBb<#`#kXd;%boCE0`~d{W}zexxYrn=KZ8_7F9i>8TxkI^{hgynQ!hz> zOxwgR>JoAeBQH32AY|K0#aC0#uk>TanY*j~&OeDdI6Y>jX?I%e_{BD9056S<`+>NS zq*83y)D-ZKA*kSO^<2QoGipxVSp?Xs<@jdgFdW>Hpex%M#2k*X_y@A_LIu8{=b6<2?+1X04lMQ9BM$} zyCpFdV+Rhn5E|W$6NP#uGmO9$Q0_Zm5w5`~^Smz>N*81QRJT%cH0`CUW3-?~_d!^6 zl5NDdxp)ix8r9+?ukVkntwF#fI@2Od-+8e!je)o6br!>YHiZBHeyv84=m@27O4*!* zN?LwKg_zcw8C6WbJUksBe9*+#Q=G1OzhKg)?AkT@=|Q_|TT?o-yfuby&{uHE{@n-H z)w+EdHumGBkuMvR?3V?)_er2Z(3C3GNIJ1HLRVxB0wjYx-u9Z`*c(v2jy$nY&*Q*$ z>=0$C}A30S0`j3@w2%A8A<1g;WiN3yZX7`S|tf_@?++o7&17^{PB8WB3&oD zk(rN5F7KeAlc|<^$Srn1>55}tEd*}rQ{hEJUEYE>Pc6Z~ot9kNmL(rxdU8gxhE~y@ zFF8^51CTVEJmnK;W~4Wyc+r15`uRMyQy#L=EnAyg13w*uFCsF}WdvzlMX(A8y84+O zUiIw}{!hbr8XzN{FuZ$67T;D_fCe?iiAt_%K|0ak%reGFpNXjf`6dVC8&)6pJ9(Yv z0tCdiV3!{%FbY$kS3U&7)ydGLSL5p(U?v;HpFce$?|2=5l;^jMcJK~8JYvX ziay|0Sv^+6(PGX_<@c*${pEb7BCu6vo=MmxPWg+j@On)u%L8Q3e)`2oCW%P7dAxZd z<=B3x`Tv|Di%B3gjAT78STd3KCpz5KO$!z}r(YdZ)V>UDk!-ywmlF%PX2v6GlK0Nk z%4%UGQCmGPdxsF$l-g~^^L{)rcF>Z2K)>bdw*6xY8q|gt)DyEQVo_gnJl=MwDH8!r zP*O%zUi(z}KAtq2467PEWi~8@<6`>n$HiL>S3d#-pYx|-^1pmdXVNtG7_$z!O-C4= zeIQFTwY~k8!wLo_U4k!LT%?`bwfdg7=j=H^K)H@DFr0D@N=+O+>b08z$xh_gX3+C^ z)$xxu=+nMe#gBocL=8C`iRvk#N4B&$@drlsC$>B7x8jJHz7?v6<~QPBO~P|1hGp362=ykv%81c9qZg6zlR*$X zb%vwyM1Ad>etMei(6?P)7M=C@(Lq1V!7rST9AV?&X9(YrT9%mIbQFeB#yG^3NjoY=*t}))UB_UhfG=tt?N$@ zN)tjQhNMxW1iIdvJMX7TD)rd=V(B34xeuk7%{059EdOZZd>-+51(AWqAmOa`rYQt5(Sm3d}>bq;B%$ez>1?Oo#B zk^p+=bq6p^09In2TYq>dvGQQKDoTAW6IM%TO2-L^G}NzT!>(r7;m-ltXLm-TMK(}j zM}ejJEU9ti^18o`W%Yx7IjA6PXec+8$ZGmX0^xMX+%?plM1?pxz&8=X)$f{6T&hWt zQH7vJA6ufa^aB@W7)RDL>Y!B#4v2?;-5HvEP`r*Y8Qtu6dV9QPkJ1oj*`++S?-5;_ z-)f5|2p+#$A(+6I`kPV`Q?;G?7v_bbnsXj7hZ?rj zA%Rq_KyS>=3*JYP?BI@( z<@K$U?p^nMgRWL`k~~$slUd7ehHSHuAs1m5N$U9%Ei~G6_inq6y^%5-y81BKy0A+J z@JIkz=Wa#T-WrozTnp01qzjF5Ij@TqEJf(na?~Km#8;G?-J}KQ+3rkqjX{8q8`W_k z5_h*|XsgOa7>z=myeXb%t>!M3N4mCSp*2MT@!d5%JwbRj2lwffCJ}~g5fmo5RJDO1 zk3Ndsa=a2OQae|>c684Ff4+oOwH7E7P4zJKY^_IAy;RXTl%U?Illj)8%|H)aVtm>dcUOw zQvsUaAQ+(b&a|5Nq64CTg__W!zaCF z*c_d31UJZ4`H)40&NhCMkEAFE`GXWSLS%CzshEadad_~l0q=Trhk+8rgsO_6gDPEGV%!&L>PT+`gU5{{;m3}>x8 z#(wvct$Ir(ecxk0{MxI!ncwE9s9}(#p3g_X?ld{;DL!rc0E;UzOw{t3mzDj?WBElV zz%~)0RX`IK=IMz-huF!g!4am)WLew{QU(`L{J4wOsM$Eoo3r;_AWWa`@KwFrk*_5Q zr~QiXidh|9#)_S58em|B)0xn2=Yorew?Yk4K8Pvijvs^5K9T1Hxk$4SO=mgRpLABl|Sw8k9P5Uuzi z-7Q%(Jqy?jo1cveD|E#Ln1*vU%d^o!+d5omwD+i4)f8{@P?ZXqOL;r8UcBnpr{xX# zWvu+3KYUID1a6{F>e&YEbjc{)U@32)?V}?((^c}dL_y}m)D!o>v)U+=$RC_MOE|dS zf(AH18=GLfTs8-A`@v&w4BrAIzs7Px3D>xyDtYL$_6)uTc zCDZk`@q^Iuttk*WUpE-;Efn-B2|eumHUieVzga-YmMS2mdT*)JJ0Bng@sRwhvXSGS z4eDP0F<@lNqQ7|Et92mc7$Ay)EyYkbFy^F(jEVI=uV~LN+*?zw)v&NOZ50uwkvJ*Q zp*}QGQj;L)MwQXhfYiyw@RDAm;dgFHI*mDfQobadID|3GGw^vdq6BOk6eBVd#}L$n zIgz?Jyr*eowHu|cs=L?q(HP6~FqhUIJXQtj1nxa76MbQ)UOXf|cx)%G%5zq%NBMG$ zk!|RH!9Y}&?f=x*o&K&!+rA*E99kdDIA{V1?QjB0%|i-5StObZ#AUIWA$S*as|-lI$Q8)<%h9`J=6kpQ6u#++N`v z{6QzLI_j9#J}_++c`7i|J)n;}v)EXZ9-ML7GV<8BXrxnQ9*az(N8Z$5&LMNW?rr#C zzx8$1GGozZgv={aJqETiKui2UM2He}Z<5nz%40d)PKyaFXkUb4MQuyt;#A#=mySD_ zWT1TjrC6V6u6zoa@|03=yQy>;@}uN>+m|xij!*(QVIsT>XC?bE+87;kz!pPblU8&{ zz6Ef!F;-t9c(6G^z@#!-haZ=jbso76yeMm-@+hNuR4*ZSNV|PS+R&)aZm-=iljllt zTlgE>gpBWzG_g-ASx4`gzF)+w`u@W=RBKDi;r@I&ru;zHd4l>h#|enjI3u2ly@%T~ z>;e*U_LFtR*G*~13?8ow-;q$~wJQ!T9ot5@7471mi9pomB{CO-U+!q)GoP)DvU&e}5Q;4EiHUBbhe0;h+MugkI#4P0G zXr2FaGQyZ`bKs1%oqC~-Jo;(E7sA^g;z*=^?RwUN=~Iy6p-nP`weLx7dbjsr6~?gg z;1J_?j!E+v6(Gw- zprr58DS&*vc1~5U6M$XarLms;Jj7{aweIoJ%F~fls^?~|Pq8T{{|+RKzmNv_ah!k{ zMMM4@)tV0AZe@q+6fW#^Uh+C!N35^uc2|ZSoZ;f{F_(u$3#RCRz`1dZ)2!DV;mb87 ziK?JeB!q6(5IY6VyHzBRK6Qf#Y2c-ips1aX(71#36Fkndqr;M$LJ$%T5Esx0qRha2 zNIRiX0xSYU+eSRVk}CsU(}bb_hO?G~T`Cp$h$#56pIyB&_~NqU05<))-G=L{U`T3S zg%J@rT5%WKb{?0-@yhE>oEF_oN6b(Yv1%r!KsF6iDJ!`@Xs65V7{-m?R728ahQuYD zlgd!X{kH;k!CbVKbzNj$hKU>(EEd)S=&9x`^ZasuD@Ya>By6|n>cgpcS}gsnE` z`6do&0C5tC1$C1w?SL^GVXz7>98QQkF6o;bd_462A1=JBd6FqQfdw%?Lm`RHr{QKd zz_i{iQxA`4AAOg?Q}q)nUbJrPKo3#Ep*Vw2j0P1v{E!XbU~_@?7^SVx+p*P?c@L;3 zCe5~9KJ>y$0t00Txu6u{(kpcQ&bh9R-^(^c+`9Ru&=cFrNt8V^uBNW1V<#8=^W7eIi~H@n8|U<)aQZySbhbZ_R_3LR4#99ZJbCU zg1R_iKp2|cV@0I?rDL~~a9^65{`JEs4T;je5#vUc;sWqIs$6Zmk(luVe`N+!B)`%2 zYGPXAva+qkU}<6`U95W1C$2gNR`q(i8Jjr)6A0K8Dk#DDr$nzdW1~H>swq&I$u;CW z`heB$6-PG;Pqq&ldSu0JBhe!kfEe&}McEdLxFNjWJH44(stf-ZZT^2BiN(3}l!%z& zD48YmwdPec%M^)4tuRP@c#n`?^WtnYN=3CDBxG-6<#a31x3<6ba5&fB@FlZdU{dTd zBF9qlT#t1VAXBkJmWi_sghl-L3bsh9l)D=mwDqnNsH!1NQOdv{hGl8{e{Q=IH8<|B znP6`!%|0frp@Xq0bqg>vj^A9qp#bnj` zY#ztfcfz_-rbbAX#VY2%ECC464%?k328JIOiuL6>u%7GSBfQ+08*oN)0`2* z-xZzCo`@M5*h*4LraU&QS@wCin_AD%CEQ)L_s&ggqc5(hqsJ3CM?%ty>ohc5SAu(d zX|SDNG={}J8w}z|xK8j>DOzhZxX{Oc&o>nLI$)pt*0(0MK#!y=>vWJ_&MyCO*;mR|gSnfDW^ zH6xO{9Pq33DN?j5oC*VoC#F%jklo8%RNsQ0J4TuXp-HUK?xH5w?erhxc7?VSi6|dO zt-+QHlr1n%BAajuFZg@TCzS1WlVRevyudvSmq(w8EpeQV2Y@=kb+Tg*WF2}Hl&)B| zRJ<2}m`&$XP7nAW7R{=*{KHO;63qI)c0xcCpUWl#mV@2NX5v3*jF^9Kz6Vx7k_jza z@^E6jQ{U&Rj1v@Xq9H!0FS{WPkgx!I8DF%dxl^U(p&w`~hyD@&9f{4UWOxj-|ySJVB3XFcih^gP0RQd^@_L1xQP4UP(VFvHr zyEWrOD#ozzPrN}`DOYG(uW?f2oh<#jI0-9f$i~Hq6@eTRdhsboT&lq%<*f9+|2H@R ztfV1ZeieYGOpmCzW}J;wEw-^mjJOry1SNxpcbM_+i{rdsK* zaYpC)OaP&!J(V4{EWQj}UI=cTh(?&n>W7#$I^120w|~0rAK=~-{&^uBSKML(xENiJ zQOQ%WlrFd26zp1IL=uRoz}dD8kw);6@OGjBo-B`xeNp2m^Eu-jG74M&KR`=U9CpIr{ve;>xlLmpg6=-GgN9Kg zE0b$$2k@#%0$0IH^ov4?BtLNdoV0TTJYs$B`6Yn4P_(a@IEY{E5lTM;g}ADfC!QRh zhi(v(Yjz+r6p@A*Puh0?fNOZU-m2wwjdT?8Z zDj-jYE{1oRl=-lQwmyg7Vdh!J)h!I7F{+izH3|=}bBSHR z&sd;~Yz{mf)p-aStVco{3juViuNAMaCc|DuI=!Jc-JOL8-H95a`?3TYO1{Uwo+shV zu=06PJBIg^^Z)}-Y;(7^;)b$C7=s^|0)+Yh^Q)Gl^0ViIKB`$CHVX=+caaoGBrXw(Y!)iQ13|)&6D**RAHRyL*fN z-|bS%k?Ls1u)kx>m48OX7g()~pN!|y-!U*d39-#Q<}gBur-Cd1zXr+^Q$*Oo*)BUuu4woDrxq5sH;;n+5as$0Z77I0{Kk3 z44F^IB!}uQqc?LibEjb&<&LczIlQgLU+J^6UCa|~kv2-oAdzB}XP)${PY@7ko&yG1 zC>FLx#U>^@9ik6rPjv_y>8#c-+lz~=WHp)$W)v){r<+;TAm_QcBSEil_MKvk(KyiQ zYFY_7DrLY+`iru zM=fh(!MZKAxz19ob2t$h*@XTSV*T(=z{DKDJ*hIv>AG7!?Rkzpu6Zhrc)l4p37S}Jy1&~r=pR*npA+*S zDtME61*uH-c`pIO`$fTD{wfMEo(<4iC8K7LY{j_pagcC}y6~a^!Q{)}&M|^@nkKkt zKRa2Kx?W4vJ4Hn9In`In6wVhaRSvK^31vm}85l`}|F#7aZ){^ZZ~8*piB0*5^W)_E z7DFxXKWEkhSe3#JastSw%eS?)&X5;3Xb`TXXr-l~qZz2{MrW znntO**21n_q`5s#58XEo0NN~5E6xddVOcO3zqf_z1;B$ulZ}A_O<@fIp^S(-B8EtK z*l#$o$tI53K?M=I$|@jnkOUuj;k}n=g?Q5{e@aKqr)#~uvMnV2J3W$*9L@a~AxfVx zwrClIZH8AoUNrYR2lWVWpu&rsw`*f8;Nr902;|s~fE346lzVy&RtKk*b+0jlJL7Rs zqtnaCxr?=d%4HxlZ{?3PZI|~J{Cr#?eCKlyae;$HJV@opX2DHNC8f$|#Ciu_t9ws{ zSWXUjQvK67G%Do)-Wr4g%xd+96K{}z+CnyURbL{AqBgyP>n>${OB z&+kn7OJD~DE4`>ib2AAv2@!1JD&^Q{GopsfbGxjA=*1BaWo`WI>E#|DJ8Rfv91?eN zIkeYaG?Em9zSLSzc=A^};{7hi~8Vjd|SDprV>a@GFRLty#2%^z~Zt3{Piem1LqsyB0nQEYN zpzXBBYt+45_X<%5+f^m}elayC>0uiH#gLK?8?0X`oEZkZa79kZqC&-KAhHXie*csH zrfQ_Bx5JQzY-uFC-`M?M-lybJXpaq8Avt4VmwHDT?A5>L{SW1;`ZnpNHIXFsC4%Cg z)ZIj7Rz162^sqhq8x@-GZs%5i`S)b4OVjAP2`Vi>?XKPJ81vNwfWK8e@AFy+sfzL@ z;eo|!7m$^%UoUoa6?HT0p&_kf=IblUnuGckdDf*hEOYR`HbhfT zu7=BHlSriGlSHpl{&-{XzYioagpkJmnUP{9oqX(zL8>|(kt(T@=6Se3XV7~wvr1>s z1#N$lp^3j_u3TqRL}M)-;-V=5IMq884$+<;iqYC2cw8h}>eGR0oc4 z*m%$$szh16_hW{0h6)T*u!C&v4$25ALY9`_97PPMPAuPT5m9RrpdvcSw49=0dYtrS z6T<)3%)CR2>EC4hayPtJ;)RVQwz&E{%-|?7*rNP=otMnk2E+)1C}3W9-|OknYw9+! z>%k)1x2AjcyeyWJDAlT;)in8lhSedUrLbe5p7&7%SDkC8gW4wfOe<*@&Y=z)zx~tw zcgUVP(Tc)>f*qAMLojRo!{c$kPJr$XQOn z&kHO!MB_T{ia92e*eXVL%Zi)Ln``A~?kXxx-3wJ(!qGng-#7kGQK`jg@C;;|&{akG zq$?|QV;3p#X^Mo)YQYDpg5OB!(Rp=++Y`m$wCRbSaZ?_%Q7>G~kw9{oS5e+uxTV8^(+2Elt5EiFQSVP4KK(%cRm{}-u};G^ z92bQLF(js(9*^tA@=KLP}029~s-7}@5;SQ;mHeXz6%q*!4&EJfa~ zeU>I_?nJH~Oy)*k&mC66y7e64jdJ!S@Mi?tOx!=~rRSgJUWWK-=6N7X`}91(v*iR| zt}E?8d|;GA$A~0VDlK(Md+f-6o+RZKzW!SpNUUBTa36lP!ZRpIt zy=KC_*FXjlmrU;r^xZEOW^js&mYBM6@%y(;!GG|`jo~g=sn%JnYFFQyHPf!00VRia z%s^(_cgX2A*ihmF(rKvj>;9r*xemG`#7tID&TvMUt=Q-iEpIBhp$MG;TIYfsO z?21^x7~Tvw(s~QF8%kCcE(`;KwzTy*=7xYCHO$Vco#&AE;cZ~hJ7 z0eN05Wm^FC*6;c^Uq&;g;}GP!5`=;J?it=~^!T;&l9(1I+^@Fc`JvUZlu z%WDkvb-M?xrhm&tG0q+SOQvc-g#ulvKG9{}zl)=xb955$Ph?Qwn`=1+z zzTKGxTk%I|5|v-zPS7CqM;)(k(4{k>CVmmV0LvoV*Megf%8_!5B6Xo-G5xA9i*@!1 zrkl+-%R1RV20y??g#OoRR(BDN=9j$skPdu|?GtCL58#XFFwH^?3qx%fIA4d!p*rj)874MSS?#MU~^=FZ>E#{Ndfn z122K=n`~SqnbduA1RggmG2)>kXrd^~x*bWs`M%$?M z4>R{lsw-%19X#+jM^Uq)y1kJgBYHI26-Z2BwTcpAO3-bTo3mK7$;!EL z%63$c=)N?X#f-eu)Xcx%?srepkWN1@q?8(g_({|av-Jf{5%O-2->ibYtPD`v53?Op zBBmJv`hD?{acu0G2+os$1GYetS0_gz|MAZP&vOB{MXj(R07OTNBxg?u?`e1zSTel! zd=<5E~vGQ{Q_wJ`Y%eW5W`v!Gp zV?3v%lL);ERJ`^6>xvW0v6k0chZ_K<-7Ms4XS?M^}4TyBAi1b@n~ zup>rGTTl@0d7u(=tm#H&s57?roRC(ESjaxl(7^I>ab8vH1BYq2;sDlY{qzuY zanBz~dGX;hg<{yF^~5dM4=&L`5;uF$b#w{-j1zUR;rt8!-R8gVtD*BIcszAN>mk2f zbF&8J)_O{`hLm%n;z?Xe8#6#T!&WsM)8T3t)A{?!ny77+WFwnFq_47*Z`m`PMyyxo zC2Gbja5p;-YRJak1rbYx8+M_Vrb!uJ37?Ds*h}SJTWHn?%X*k4yNA6j zXE?ruk{e^C9Ei)!t{#sucP8sc|7D$`4}W?>MvVS>=@_dht(QBb*tAo(cG4AOhK_KR zFp>P~4*FpQuQ;NPJQTeI!y4FS2givL+_p*D23d*3N@=2D<`=Ef z@^miZFEGmIVQ8##$n+$}caAoD&qZHD1fQGNx?*9cG_(S0C zkk9ofRgBT{!WRQ0zoI|N->?VzPx(c3hP&fxnpah zZT42)(-92LT|k@@b{Wi%5IfQ$fEGCI@eP8uCC!43lC38VHUgT|y^{N@SFzSG|(gy zsK(@34`<@35oD%XtO{tU2tx%hVO=SU8?k=YO|djy=M`lpUEr76dRKU~q=+$A94K5P z`S)YYGZQfxbeQBr9TxB)>#|nqJTeZ@kLtVQY{hkw|)rTVpCcz$Tj zB!ZY=%N1qSqFz1@Vi0p`7;rP7`xtI<+po-o?75HT3=c(ltN}dWdYdS&cUqy$#P8Lf z)V5i(yJ7!>E~cDoME;)T770J@oPoNh7UFSHYR>$XM!qGpRl;4 zq3Q`7|F$v%}Z%axZLpoJf1yfJ%~z)OcZsV6gOfs_2vU#O9__utGX#RM;i zAOun{qFj|^t169-3I-$uc|_MJ0)$))zA&E&d0!y_hcMBOwhpe|<8jNq1php$^DT1rJUlSYD8rraJ=$ibg(ysJ`A&aar4SPSd4s(TA!O z)0vN}KTYp@M%9(wNOU%=nuLXrQcQlSPC+5&Dql~z`29l@m^xW!Za7ZXoz&kIomqj> zB=HjPJEOrJs8~lOW|=`S5zZ1d41zX2!o&$AKQ^Ta&7tYQ#~a(3Ox0lM_LWtlx~~#e z-&4yd(C|Rt5x`epr*W%SOIbD;)fcmtgTC~ zK9W6pbCeZ6&MG?o<44`a$34#S#U=pwr5S{v=d0bZx#!w;J!dJPSQfJ+c(NY-XExXB zRh_Ec)P!pO=9cpAw#N<~$vd_ju)Es;!M%={ZxYpyPfq&_`ux!U-sy99f2hc)uH3)s z;Ty4Pi1D7 z>SUaBUoN;_C6p4XwI|}~s;Uc(soCmIL6gQz?B%tdqV+wjNwKCXystQS9sa0@Grk8r zKZv!K_zyPx#SYq8jOtn2Hy&cEa9JY6w!nHSw`?g{oZeR~^dj$bmyK68U~B#!Cz=VI zJII&}L*)OGH|}{8*Z{n!9c~KPa0o`OBi0a zkwiESiX3VFc=gm^l7zgasR-D9JlF0z)HZi#(n5kD$3ia+r5RCUV_L9jmDOV3t-(cg zFb$TAFn!yV>GM1@axG* zH^0^d_T{l}nOLzo-$%c+4Gy|3xE6^Xk*6RCx4L&Zqpm2TzMggFGM`hvQ%pMfAu#EM zoYJ7hBo}}d7%u{$Tjs$9a@v9~P_vtKMA!DMUV`@(Bl$e)MTf8592zCELpPSp%J|DfQn9`7G1|g@t6sZvL7mn>S7Ji`wm{`&P z_9qP5<-a9Rm!aq+LR;WM3-Y0Gu>Jj3T9izMcR)=w zAVk5p)hMp@2<#!CA>PNwKicA46oh1>&q*r1`#8aV@5 zMdVvGV+FFqE}CQoRr`aI&N$mBl}JI15IBP^^QiTP}wo#e4|2=qxlO5 zM$kkTl$ew*8w%gCW6*|9q%PvYkrx4{0cp$6hisj=w-T#i=+R&2qGAtgPXjz0l&#Cp zjG;|u7qq852Wj{nmKhyt<$wt9J+i~wb;oja_ zvQLGkZJujj!q5IrZpjn04E2E~DUKeaRGZAV^QQE z1sR6nspDM<_WwPrGmUd;#GXIee`#wH)*=?~nzhaL5Rd6^J{R<`zw|9waWy6972U>S z#?ODmvlT0C=j|h3%E)>F(hX_%@HUHt#)kny2*!yXa6uCi)+P1&Mr^)6nH z!rye}ye=y$UMpMLXA^@Ga5{nj&;Cw54&p4CB*N{1d5txJ$-nMoO%0n{;gkFZU7WKt zx|yoNcy}ENBc`!t&3_;`PnWjsLA<__7lO%>41lZ~M~p`uU|WPtItpFIg>f2}gb(D= z11B|Q zgM)ZT^S>&9Z0N8>H_0YV$;MG-!iowThkPRiO?UqX!U!(5wHcHe*pcQR&{=1i7*K0U zGk#p~G?}-u%E(mN5E`%LhG`5wgCDl0_6sH@UyH$N+|l~#=4ur#*Nm9J@8i@`W5il_ zMALW7>b#334_VCQIZ^V@|BA*(zhdE< z{{*y@npice1XoNNCUq4TS!_B+9NEv{N#v|s97_$N#psl`xuyb2MSrXeXPkg)PTIcz zfTpatC`@ZUO9|<>GNdnnAS#^|Du`WHJYU81!pm*qN;;7-8A4_i)3`)w>MFnwS zg`yxb`}pb{0Ps(tbII$=4H2n3;`E=Q8X}7D3WV=3fwV|PQv)2^eC!$PU|)9fJwds( zL!7)t!>|l}eCq6D0S6u9gdj&vShK{V_KPkE@r83X?>u=GG@0Ou@ErnD{%!wyT1yD)^Bj)T^( zgbK;+{VY&&PV>Oaakf2(<=ItZmdyf;%q!Q~v(WzoYaQ_E*k;>$0tAu#C#aNg{8}{- zr!~bX-DEuV0Eq7~?Iue?ij+F6P+e=3lS7Gwszo*&Sx<-9f7n5$zQN zhbfqq|C7!nZa$tje6grCSqw4SyeslSIb+VRUvGBCHWrY^5CTr3y^_pOY4)V5HZuZd z$HBd5&@H#Yy1X=c{8C*jnjO>Y4rL1O%Bc^)8QXZ(`B`Z*xTHK*9E7DHY0bpo*Nz+V zNY=$iG?58yAMGTEuX+EEFTW=%3I0;jjZNwWLb8N(6Rm~sX7VQ+RPHGWa$->@Td~*W zV)S{Wj@4~BP%8Yd>zr!_xTWwxJ5Qbp{60WJ;!yq22(gy-y|dM>*Xv9V>6^S0o`Zky7ZCX<>RARj-3s2(hU7g3)^Y;5ab+#$t64`X=jl`RqFl2*`jPQp zy4s7QaEePvsywC?v$=S>dF(?Ol8PGGqZ+-nYQamC?oI5s@uCXt3_#fL-^A`OLYb_IR+QY} z9H+pOO*;F&G=Ehg=uHzE4mG@$xybGgUK`g~emu5BsvhnNa#AMdqUFz%|k|0`VS7;N=3pha3mfQw*sc9(1lO9uZ0SZ!h+0u}zyW5Ac= zkf|M5(_j{n#2204*->U#6{P7@)q*O6dIxY;O;@#;I(iubX@dsdTDoVX8h{ZA=&W{2 zFa9i1gXCeZ;BrEjqo(h!6b{o{kqMF7FVb>bZ7B|nw~=@sf5^$3_({maZqPMH&yC^RRBMJFR8^`3+uAunTJ`+1X?gBYF`^_MkzKmUdi~5I$U>!Ax?e3dPIS&zwH}RL)F8g zYt)&Yt{`@AvrAc_7Hvl?&U&-}+mGH*Zz6|zz_4En4_X}i%EY+~LVSU^;ibf`VFWFj zH%3xAhrz|O&00&|xiDYPH+qKZ2(`Yy6??^Sj?C`1uaM2p8T;%J|VS3gTHKg9p@-M1wlHC5m|1J zG%KC032w1dUWbg4s0%`H5bSt{4_iN38L4Dt1$G;5fb{u8Tt?||NKjdAq)Q6SIa1H@J+>%?F zvdCa3=m&~q8YS?0RYTpq(50#Wjgw*@ti*2csq4VL+m1RQC`_m;K4*-c;pdt05l8eL z{`mXzO87&$;E{Ps(=lIdb2OPOoblqmmYYs>Idq+&`3S!ct5xiOMkV)NV z=Bo3Bae9F|o81_ORULDzP?1l!%#*#%CEr|)Lz(?&@7p{gUHG&*b!a8ibHoz5FN%9F zYM?`6AI%+@;QT>HJe0TGX_Pba3+%Obk)R6g57(xpKQXx4Pw)>E29YCjM6nqf(PM3# zj0;6tIut}E>xIn_hLl%2QVw=M2k(w#?O~NK<9HttIJ0dFwscpFuuXXR!&>wi31x`j z4w*Mh7Xs8#4P|}8TJULCnpKYjkEl>8XYxfAO@yi?^vE;KH}!xfb3Dn)SWO=73*(tG z%IGz5H(KKIK%#yh0ijcJlXu{dBrY9WE%N5K-=Vu{1nX0<$b!9qsRvBMJJFT+;{t+YNM!iF@!!#UUBC$6; z(I$En@<))isw|5UM@vbcPlp_51(V)A2C_srrt5YN-tl?n=PImaE<5*mL=K6aN5sE2 zRoKMl=-aP(x??l-$d?g$?r{L7xythMu(Dz()>a`VTrmUdJvU5-->*5&3^2OUe&?;B zQbMkcGgLn~4M_7>81hDEQys|;eR%B}q-#le)qk*Gp{j61IRnKTsGG2F-=33=lSN4U z$MGiURY?a#Sq-?APmL5Na6C_6LD@01$St^rUCTU)*IDSn^LEV zF}DAce47a`DWfi#4-FSGAp5#ZO+Zv?N%Vc1<{O&DdBHvE4$CAHWao{cNc2|`);If& zA%Wxb3g%Xzd?6GEP?l*5aSoleLG1A*i~TKo{!C62ZCPtP2Z$q8D$}oT(S>+vj2%OI z3kj0{mdnN$?ocg$o@YA41pmcZVF1Oza6z$0u3D z7vj3WAUQalXp@>Adqs~wS&R%&o*@jrwL)6^xg^B$H1O?iMx4{}dCa1x0H@Op19-0fRmH~t7^qZW57ei3ZC6*i_eue_cI=WlyM4gH@q_5`!m`nQsz zxl5x{f@Cf^V-75>7$ERVNdwbCAgacIr}NxDE!<;tYfaxRD!pk-o8>^MSSo%h|8ZVi zn?%o>En3!@2FK^OT=tdQ^~c;KDj_Hr1<|tcF=o$&7vA$U^5FCtg}^RH|3-66seL$% z4yLbjHqUFlgyUOE8BQ)lTq-^HwcY}`mKp?oG1k=v^fpExc1soX44B?9EKV3ByWTIczC8xF+*G+7K2%*Jnh z;ojB@DW{bOo41Lh9V)FX&?OAVPV&pvx1DVMqB0m9;aKl}k2Jt={2f7MG+w@vTjQ#I zL@pIvL<4erwsZ5fUJ?KD5*!9&x`);x&xn9EER z5$-+{$AD&P{W@`m9wK$-BzWu4(XP*94cGAnO-Il_)hOoN!|z-Jh4 z8q~+FQ{p}h*fb={mn4782L6a0wq%Ij=-WHWWVLoBb1g`m;ph>|rKkjhNnG?GcR8}$ z6r)0bQI4x*D{pqhHx-n=6`Or5xR`%5ZlAcW0~Jh#5n8_3aV%hga}$heH+JUcW)X&~ z#W6e})w1KPA69@lJ)({<4Nj|lR!A>w9&mtVc+6lx z#QtXH1q$x_GAqH<4-=2eqen$Y-)2d2Lp>FRL4>`XK;;rmyqoqjwjP({Zc(Pab`xv- zy3o1cnzEJiKQB!~-CWONk71?G2;2BWIS!$y0NUpfs@q)4=%htAZg3c%>|m^+Yst)_ z^lMKG2x7HFv;^hwuw|?L7W9GN^8G-#x(>fN)4Tf6{~z_BAHwxO;cNa}1!E-$#Nt3~ z9!|~sce0IqaPkC&18?(d5hog(^0uf@`H@^PFBs)Y;~{;LGQ4|!-<@mQlGB#MG?p?$ zd&q1PzdW5aIAV4GPJ)j&S0E>Jip1w@)AW^%#-wpaNuN}CP16VG~m;tDdmZ22dLbeu^c zcaN*`CczG8DhvnVwEp_f9y)@b0TVIy7$IHBs8(b!d#i9NvTFM=3)D0zIP$;2q{xJ# zGve%Le0C6N0S$^1?SK@)#{#QkFu zZ&Q9Tv92V}kBu7t1o}atwo^G$f52etB%qr$#be zPTBgU+no?sCQj8v)KO?Qlt4pYco6pQ=aXWG4ZD!`A3jPLvg49apiIw__SQd=`~QQCThvFQsS|$`G2Olt zm^1vo-m00=Sr+%|4AXCz>}bDash>Ym#(IKG@@~IANQ*FjS8^1uY_11Usb10k_hkl% z_05fa6A3<0;bxhrC)>1BOEbpF#0wESs5DB+qY@B7rML}{OFMHPVk3ygbkV)9wxRZ( zPF*N4!Ah|tir{1(IkW}$>K#rO+b3PuP*1Ha4|!$(cnVHMA2r8>brvJ9n{vnQd{jQ?-(vf7EngdPw;ancN5R!<(c93xtIVLp5kd>V{ zVI+(fvzn=y*XO=Z?#B{!u-Rf5;g28#rS)G2*gN%T77>^sV_7u}I_80AQ4KbtFiBm` z+V{>wZXf^-;7@mXESfdtLv?m^+3O=r|7O%(5As;j%n9o8%Qhw2SE70lFNfhNUZwOy&4fl-rU_74bzu zE9Vs7mC~Gp@ogYB?v zrwyU86`}hymNt5H68qZVy48CO02>*lBI4ZbKT>B5rV!zoJ)-3c?M=LwP7++Uf09?I zZa?uhkO5oyR*;a?m8;I92?Rm~doirRvVdm^HS1FQ=|^XiV*_Wef&44GwPf+*gRdr9 z0U@8aEcY*X%WZg;!*==BLlx}xX5tIskl-MdL_I#l=92w zrr5|Rep7^NbFk;n8h$n6YySo^Q?MA-Vmj-!a9u-!df=H23kJ5#vA7EFrgNfD_kAs7 zsnai97~IncH{KJmW1{68D}@0SD!4!S(>! zXtd`(P650?qo|GMWqx=NAFHI(&-!UrHcH!G*i7`%?>|?I<4|hHnn2v;1 zC)yS2)PNuLBG_tYb)DdV5C7ve6C`{^H<(Sn7xz`EIjK?>-Uev%WJj5pWGzMpIb%&P zG;#n+%6g`qt;^MrVa`^NCQ0!A^OxHJ6bS8GB!TvuVtONMsi+n9|CJCV|M;3J=i#yz zl-}Q>=}6rpcX5ajIqc1tfEHt(M~)XEasQ7*3!Q!P)#&99sD3%I^98j8uktMM{7`Xo zanJOXSiOIvqf2tM6n%<(K8}S60QG(oT8erBz-o4=nnmPpIH60YA6B8m8O3J{ANK?(3#F5iQAT!mo``hc zt>0DwRG@t|n|9L_N3Qo&$hgv|QJ_AUB)z;x7;rN7nk5MzxA$zbbgwBKDZk7hUsyOq z{l?hWjMSGH`S?4&1WCu?(3NvdeMz&bemP1l_5f154HpKNZ1dBn^_XDc+RVx>+FbP1 zTkHwoE&I?zic_lUtUsIjgZZ!pYzXp*eg)`6bhpKWWaA}VK_U|=xsiC#Zu5yovqrOPgIx27X$m2>oV9})Clh@=g^WpU%YW97WrLah@D zO_#7ek4BIGp=x9P+xx#+>Ku2&GPk6H{BB4 zUH(8xNQgl~^*mF{CK&paaSLtWkFliI5#y%=NYC6}0sO>QovIZl{e}sheK`uFnPn=Q zAW*w!31u9gZWnf9+UeTaa#i7E(8w~#nWM6e_81~%lpqd=#-N76IRbVn{1X^D@HNsG z8b&H|*;B_VmO_EIf98Q9BE_s)laR^HX)6&f(Z!ig2{RZfJQWS?xkv4wsfU8#J=iPX z+`7!goj?FHK+L~blrdOWqs82N>s-w;*WvqoYyj*WrzjCAYrr~H#$VdS;B{ntg+o5n z*3$TP=l*7Hxaus^s}1tYZYFBf_zw;Bf{edWue>4$1H!gOgn70OSrIPi+48k8Fr+%|FUQf4V1#*&@S#E5{I6RJJLI2cW6e7sY*yR z`EKz>8h9+~acFtV6S}#)24*Y`4ji>;<8 z^SedM2BP%ja6n|oh=r6VTH$+I zA&JXRRRUp_4Fek4FG(bcKJ&q^xbNpe0TYD}=MQ<$?A{V!Q1*ZExMhpHG+X)eP>L&A zDhQ>6WIq$JX2lR+S#7W>)Q1MmpMRtvD3c4_nUA$;_XV|D%b`@L>U3S0Ek?(idYYk! zy3M3AF9?@_2n`~KVC%m!UTV7-urafKd5Y|~=cpF(b;|g8Dw0JTzxy;e9mst3on5sLtX1+i%TzJUZojN2dUvA<#SGnv44<=nm zEs!?e?oON%S+v3d5%ygu3=FRg0aB8Ht3nZXzS?cLpQUl^f+Ka3o!*oR4IAUfmP>% ztPv%)CYY5Gv+1MzpA5uL#2-WS+DxFaS8IM>uT|Hdi@3R?i8f0l`eDp;dS|0}YciO= zLyv*oN(tq_Fk+RP&Ty-!DC7N-{YkkQjU!Ji#yD&?npM%)BZU0J?AWxEZn|@FoOB}% z6&8^4?u@(G09G|LNxQDT|Q_fonaA++Ksx7&kN6_TZ{NL7$gFgU7@*|16 zU&&1?x1K+?!^D}3Yx*~l)Hhq z4q;odc;>DG;xm9%31gDqr=!44JSy2B?q@D$tEMm^ojYV0_1i~#mh5%`AbkC#71XE| z0EBlTjL1W(u}?UOx}|YKy@|ot1z|^Je5T*}&q4BwSJUs-IcNN0T^$D*Y;0vKJEBu`0r72Lt4qBDGshHG>(MV2-9fMZ!KFbIUQW+mv zQ_mef7Z`e!BL8(xIf@S*<#&4N)3lar=+O8DP?BwT8;cq^bp+#-hC#JrHP)Hr*i+p>U2cj>9+|fl%VJ8hsj|NAcN3kiVnJ+=v z%>SPkS*y|(pu&(=*h{^Tr!eHMwH=?o>egLTv1SyKVf@bu#-0tN(MXjSFQ4n%3EV|* zJt8zmV4gwF^s-%vv@8EL3RsE#gg{);dNy8=eq60OI`YJpv>Z2iOv(#8 z!e@>0!>%pOKADrt34Jl4pBD;44*!;O%NB5jzo(HB`4uJ&f%jAI;w-DVLI2a3rFjXs zRgv`{$6#Mf0+U>=6D*~Y66q>Af&}^91c;o&jF|U-H7bVk3s7TI&al*2Q=8W(sOtiJ zgJOe~ctHw1O>fNZB+)G zGB7OL@?m%H%LvnA`GZ9P{#6xW`4(FtH+dz2pl%5pm{d49GB-suIM-3`_f}rv)cpW7$ zxDp~q@)5jIFPzuE$@*M9bV5(ug|3^qMC+zZ*Yl$c2)_W@LXS1gs;^>_A zx-$&naLWU@aEHKh^Z$Ap2^pM|TRxrA?V$0JCzce+`N{tbCNI~yu!nph6nk&=Noe}) z%6#eKLNyCG8WYrRGCBn^pOVJiv$^xa;54#f947Y?Dxqz^Fmb{oB+j7Pgybtl&&iF!IL=SF>Pd&A6ivW7WJvj56xgnMfVPAjg<}>2+&KcUnAzc|HL6@0Unji8_ zGHKT21IaG9gB%Q{*P?|(=>a;3Qv~yp@V^9=<5K;~PkB1@Q56eZiPZ)oEj-sng#qg( zV`X`!s06Zw(dFX;=Lzd;)4buW0m|k#Ba&O*60qwiHNlqvXQ@|xmW5KDN?E7g-?~yr z#uo{7UrCCnQM9_K%Yr}^zcGeWqB@i|32I8hi#k-k-dNZlG;KGq(P1;a{+rY}hQSLC z7*z#~empW|!do1hM5P!+dqi$IEB#7|W)oeJqQ+~O2a&oK2CQ})&f~*i3)Fu)Muu=s zj8eoxxJJNTm1`!|(yY0E`3UC?-373aAKVz2eO#Q+b?GrIWMYsDkD30h1k;LlxrE5* zhYl;E|M)e-3{8S#ctj1*>u%9wvFO~`oJ8iL$p8KKdCsK zYK`Y$G|!v-zDY{B6v~MTi`e}Bp~bmS;efLIW$>NI+12=@J$GsVV8^Xa<@%LhzVjd@wD#MmbDDTYK>@I!lTwQmtWWB>ciDz;9F z==w{k+0{Zy@CrAMMX!1cxA1-5Q2`}i=o5~5b=dNvrR_QX^%~se&VIk}w5>Ijsv9Au zl*(>mn2Z8?q1v9v{4)0lI|*U;v|!kj&19of5UzSi4^Vkua#e>fb~r|Pmf;V$63sG& z60dZQk9H75#)F>q?D;n@ZwJsyXM^wD81^uYfZTg6io;X8zL?rfBtkz{4SljqiUh*> zvG13+DsayIR^iU@92_2tAy1w0ECmP8QUS$RXkX`)yh`Fem(9}%F0S?@OJ$}$8V@{E zJjd$&F(?Z|bt~~Yl%^y;HkSluH(hao_zYauo~y|#$lJY~p||QhkYqny|G^qc9h9__PXN3Gn1jS4jVAL%)q}BWh1c2B-~Gw6+%;PK z;!0};Z(&2_t8a|6|41pOkTYT6HJRt-Px{MqqF1{&c)k15kn=InIYH~6;vLGLCk<3g zxKu_dShn1wUQr7T+rcuHpdyD0!k=APeKiLB0HLkkF_shcA_RYSn0WAyKqfKf$A9gF zJzr%ojPZ(`i!^$6)e~|`-yR7U^`QQ! zj>-JRG0Pwp*<&;Uok9nvQ?v3hO+|sfBt(Ix@s&h$t#7@lj6VjzN;DAa7iw_xJcfT+ z&HOX7q%hBVz2>{*0J$`ced1KQs_s+j@nypx&L0Ey(3Af5o?dhsujYT^tNGKQT*o}) zEgZ8KC#4Zfm9BW^B@(yAKYmXa^zSsXf^fS9ppMth0_UoK)!*-q=?Y`DgrhEu%v$*o zLrk8iy7f4{wZ#$e?qSeEOdvM=wcPAYO;dzE_qexA(16qQ!uva3GeSYwt#G|i*kTr> z1z9Ym3kf@vW@!qgVCoX4kgEsdMbJPDJGJ68*)>8dwfOe zAy`A1Q`VXFd7a89BbmvHoyRtt1q6}vLgF)YFwZS8hk-%R3foZ3; z)UWxjv1%z~i~SfjRp`7MY!L+3@Ay1<#fJ;cIak{r0blNP;e_&+fD*lOf*@nk8D6M9 zq=<`v#uk!zhEXgyk-~L%?6b2SP6Ba0Y;Tby+JRNyMWmDKfv}eVm9#^EL@pW|nutvH zwu74LVl7_asGx1){I6!?X7bmv`I#jw7xRvtPNIl94Z)8Y1cYf91sR_Ee6zo z=6N`H4s7IJhLgp`i56*YmZS&<%=3E9Ng7XcvJ3Cq_;qQ3izIOv>;6Hp#pAcyuyYZC zoNgXdGsbOJ;u9!)@jh=W{w?`;=UD?^Kns4Yo&?m{G#%iFTw+0%4wpt6-v(wZUJ`*c z_4N;tJdKU`s~F5GVeUYwvY!;dIu0N!tyb7@yA#0nx8KwIDl4yFuh%Y1HBYM2HNIK8hXBm;k$ zY?kMlwUWdmoS3qNFLd{1r4n;F_18QxJj64y@MZUZCT64L4=kEC4Lsf+G( zPhy*xN@=eZ%j|G13rm7fbBuFp^#+cx8{>bv!tOhC?va5VNEorkx>9E*47D-Y}_+ z=!O1W>+JHCjmhgDM+kBqDa?r=YS^+E$%2Q2Ozhj|0hhFH5rAX(Z7YGCOa)o9Z&~kQ z6q#@r&zD8v8K~XYtueuo5!l&fmkwfsG}eE#el;t|sZ_JYifM_@79wF&sZ(N#ZdF}< zxmBS`#6(aLQKAh19D}=jDg(Mn(7|iL1osk%Z_4>#aMILR5A*T&?zBU~$T?t*lb%_# zmPsoL(sHR!7NXPJ&UOeMGq1z8m!3WoK5U)bHzvmNw2;{5dwHWQpOfvc`V|{N= z#UF7;)6YmVkD`#_^^0bN$NH}V8kVwJv`S4}uzFDIz>1UHb(VFm!#Cf)57XstPht&T zGDB2=Y^*A|kP<2=V?s=jPz|%;U;B6I_jDs1sTy=nGnret)}HyUSv$2qpGN#XCy!PX zt1dl-4X7uO;TuUQOmQmH0UhG7pD#3hLl`$8=EA`FkIxIl4Ih=KL1SR{XGzq-Ob&vF zae{(T?Bw!qT7>9b`RXtT`xI=x!HjM~D`0Mzciz=#SF~DUP zJd`+up!$W6#zOgetxQ?|;&~A0zsTsl=&R5$u!Fz{32HPepX>n(w9%W_8UJ)Q-jXWU z_i(#2^J`kfpA>i!X9LnDB$+LYN~H@dl|w@#YJ^|}-Ja>rOLJn5MIR~)nm)*G8Zo@U z=qWr`)oUB{N}LDrQcL6A8%wtP_D50JF1Zmkgzu43wugepWJ;Pn1VMs&Y@7ryB0np0 zziYzsOXLm(jXGz-ygi^B@VcfKQI#iqeyy1pWnA|`>HS;mYk-bEG7L1#VEAN;9?}P6}#) zyMQTJIQQ*O*3+3P#TJ+b;bxxRL>+C0;NTKPf{AxoBu@X*ZEG2xe*p{JqG1!bvl5rq ze^QOTLqaS?UJoMjKwz2kMhvd8v@OVI2d7_J66^Eaz5o(Kgsr8HP=!H}9<# zy@LL#^)QS|S=?rH{4K>Sgu~?eRvk7=ZU|#jCa&6SS1?k{R zT37x(29_zY;Yze+nnv@YMbJ@c!2n@hS1|om&@LR&>sy)9p0bdXRZUK6q*fiifgF8Gz&6`3y#69_}T*h5^kwrXa_<- zJ$<`xROc~&7Oc`O?5^)^(+@K}a6P2-|I>LntH_`$EW(536lSC{yegbdM>OVR7d=&Q z2u^V9b8i@NAIQvMoXR?vGw$0STNt&*p%`0xSFM6e>HJskYF4ssICxn3H0?Z2L)+?$ zs#;bP@dYr?_)eZU2lK<3p(r^yzySTyK;SxFtK(x>)G~&i)>+oslTL@Mg6-akCj-~} zAH@6`G+ka=h!nbcj+)>_6yVwibbeiFSh0G}s4Vb7&}OO-R&!Uj)b!ag0YhJS#0+y` zI@i}8+Jx7(bnIP<5+%n*>6kq{2#io6P#k&C>2NzfmW?IbM*8>Ec}>_IOo|8y6T^bf zd9b0#!50D9)Py43?MZY}1@3;^KPzw%m(_t!jIC$;U+j))$}kA#K1oL)>eiRyOsX$- zVIrGK-DT}Ay~(bPzyyL^%M?ze;VIIFH%-8|+_9#pj-eh<0Sk|WSeq+@ndtw@TU4NG zqKStPI9{i6?yyi9Jk+xDwuJMq!z*-&#~3zj+jpZmS(kl$YeBE@0kC7@{-wh zME?~TXcg_;qDDj(uW}J_nw)5N`(nXIg4m+8*Xfj9#FR?-3FF)AK|d2&KKkm!mK}zQ zV+(7vXN{zA91;CWjAu+S2DEyQ$CVL3Dx^C9bxvX&MA)ozY8iP@ztx8A4BQ()9Lc&2 zQJgT&%VrCpI|!wDua3X+ph+2TJcJuJ@(9ybVkQrnq6;n+3HI4JLp8=~6T_&zJi}A^ z+31Son)D_jat1o*XQf|M9ENQ**x`|0^n|jdYC}Iiz-q@sMv*-ObZ(r#;k$z8P?|vl z3!#5N?T+g&wa)6wXHKN3DzCm`k!MbHCkoV+IhC{3&{3OGIH7XONP6d@M$Fx@oQaR7 zN6#?Sv|kyRawYoK4u@FhpKkmm>Evrf3l*KmD=6uQ$gc7Z|#SSVl;4o<+8 zd`%nb_x3qDv3Y9g9PVhrq$t?G?uvk}xlL2y)6|JSecSE>G}!h_ag&B&?&{7g04wsz zLLQLrOlo&<&trm0N1T=pX2yt^m&_yUj}l+D2IDz4kA4w_`wO9=P$%xVus3r6)!H); z^5wW6i67*9=pCJJFCX?hb&Kf;k!bfZJb zTD@P2>2f>mP(s|qvM6r3gY((Binw~pR`YD7vhaSG{I#BD)((t2=pKYIkXU4Lehif| zIGNZp7ZGXuz39dQ;N8mobeJX2{_e#HFPRC%AHId7TSdIHU#VH}jYsxDJ#uBw0nuz< z(FlW+h@h}YA%25c;v?I0m-dy^LR*}3kU$1LWU}8zb zg^T|lRM?SM?y#9HCF0V$8zmsVI{K+Z9Um=sB)@mBot3k!X2Q~GGtgJ+^xq59P~Gxh zkkXVT)9rp=0ONsp3D$|MCW>Y}`mT^bVpbv125h@!kLyix2;mV?%jOOX3b(*(&8>jx z(>oQbEm$wlV7BG}Iu0giYk;yFm#SXBP7I$_-wWv8JM-ClSa!5 zv^QJ!k1I8@RQkr3(S+Q*T6JD06@#QiVJZ4 zP_r#-LeXE_=d8H&6VneziA~PG%yOtbi+G&Z{slz~eMpjYO>GWh4wf9~S4K;Pdfp=D zRv+z(WL34-d zzn0Vw0KTf0dqo9R<=$v5ie|yz6I+nD@p@Gq@j9m6nC0nC+p#(apxZ%Ynua-q3w>cX z!`nmAHk3XpCEA5E)*eklKr2H-+hkQoa&7gt5e^ z#BFt8_c-eO$wy%&bJF-xso)4n<<=?OZ%M%miQNmV(62YHG8 zP0s$uUpz2tfHW+)u2AM4%vrp|M06N>T!#Ao-GGldqr}N3C+W}^J0-+w<>`#DPVtVh z7-EHB`W!{cHxn3_J;Unq9!J}LpVql4alFYMIP9vj)Ma3kQbqFO?=DaoBv3bp(9wjR z9*nOV;xwG=6!zT@0=Pr&6R1ShyxEmehZMH^aOPqMdX|S>BdO&c~ zhymOIYh@<$0g8%lR(SVBT=NMo8xSo+hZaKR(m0a9HgUD}2Gnhhi_f+X;0^g7vH2`$ zn|zBIp(x3m5ZVv}&nQpFGDk)Zc9kVCf9HU$^sZ(V1lQjuahL!}3DbH16XJzWwOXz7 z)dM9)EvRE~`2$v`)$s;5WlIl=F0Ufzire-WR^6=dY4L7A(;Iy1ene#vPz0$JV#8CZ z0GL-1e^`_3wubPPQ3yMTmyO3h=|V`9Tv}-9yIL?F&n?TRnA2K4$&VVxHi!uSL8;~d zck=s=IZU0%v3oA;Q&1P+$=)4B(A7pNg8HGQy+y6&HV;C9$yIDQ!KMlQN%*l**+LSb z!^lR=BBj8OLDIvhTLhAC-4LCH+M}x}s?>Cbr2Ko-pzwi}^=8do=QGgk4MM@*Men8K zm&U%qx>!#Y`swI%I}rOWk0=mhLaPXmDh{er;o7t}1NFHIS zI~qWgrw|%Kkzbyr_cS68sb-+275$? zJ4QV0w6}KA22ucew-50(Xlt|^{p9o;Hz$J4XGyM`|7%uzywtSc5nH#0KN-H9hK#$hj zo{n(VJKb@*;to;-dzJM5A+JfLmSQZ)%5X`6{WPwG2wRddmFa9n>d8}QsG~%K%#L3G zC*I#JjmgH$LT(<-$R6eAs=B2q0gLVd(L&;>7Vi_z46k9`_p>S~xVR=VTn^pyrVs0W zKtUGH>~utUI44F~CG0WSF*45=aHS#l;qzj%6?3n;R{2QFDFGQSU2GK^ALl|{7lze! z5p}@z)<^K3uM-{T(LClPnn>r9H!E+`jzk>-gVpc|E5x}vhK^p=8Sy`I0sUF(R_L*+ ztqmo9qCPz2RO1i$th@`MB{^asU)HW#e3(T_=GDMYfW8221t!i}K(L_w5SXS45 zhcBWi3tKpU2X>ScS;4P7&_8u$iv5L}u9+s%_Vt*AE zX4Yh8DK*!7`xyaJ+mbz-7)nqLJa#wYFM2+vgqO<0UtiLbTBJ`B>1<~^koC);5cCx z=+|Eb%5%rX`Si7iH8%*G8n659QfTZ8fYJ%s(>D#S7yPJ_k@aOhEx#Zo)D^klwjZjxSSPCNNbYbvV>Qd^!FU zuEq|;_=IG@co;_yMW4a3Tkx2MYX;+Cm}^$r)Dohf0Z&$ZMD^ZsT`grxCbskZjI;_9 zMUw~I&^5~389Gg>F(vjtyQ|ksP2>n1)W`{aKPF<0E~=c*dS@J%O9@eJn9LHD33A3g zNE!Xy$x_^xg>timU5o=xJfW5HFR`xAitjxPH&+?xMiIc)W3o zPWcBLE10le;pibc!AaUi!jRa$jHLq0bYW=l9N&~FrQh`&U^Pi{p75y)OO)2|hy;A5 z#j(2+Rt83;kG)<`>QDx4u{{vcbil3gj_S%;(C}8;v%#(vp%2nZ*D`*N6 z0D5WOBja=ZKb^CIfoRLUZA7QCh7{4QB1fe@B$JPh{fC#aC!v(KLHFZ0+Yh`LZH<`m z#4vjkSAP^5p+{Z&466J*xc>Tg_+aK^EpO4_u7bDemc9H{+kx^_K6rF6(%2=bQ!=h6 z^y75qD`Mgvm=37gq71qR-XKcCS0b65Gz&U|N`+zaWHak0g&y5$OOOc*4_vJ9vIs4MBi>Os5 zXNrgXV25E`OJdzqbY6^)>TTEz6;WGZ>#c^>GCI1NVDd0t(^{*^OGv!XHOAK7;eIc! z9&`mSEMnd}LS^&{lb`Huo-4`b6!~P#6O|E_m`9xpeR@X(-7x>K>#6Zj$%u4S-_a2x^El@KNR&BO)~auhP-Qf~2!Yuux()PAK^5tnrXh{*MfHKjy2!4!{*i_<^-FZm z>828|cqeA~0ILVMu$!|SgmO#uFA3MGVs^uOzFc*Cbdi9a+5S5IpKj{dvJI7690`B@ zTrGh~@D5-TD~HuZ6r8*2<_#A2v9RXS<;uzaPn=JI*~D(Er_a?1pyppgugWPv9rB=~*Z$&T`^`|U z6p+efU=xyr{`m0dl&F#_8I;@3%A_^)B7%CE`2;ZWL6VCQ5*@jaESp5-TI)6oxfz7H z*d>c7&9|5JciyNuGy+%VQ+CvhlRfblEO=EI70RztR?Jn(goY;hOmKIGE(e;no;$ps z#^tFnAZsStRy0$7W+=Z?sJVv5tYCN`(bgy7F~bfiHjRFRIgykp!CtfoFEJelaC50H z_r3+YA!ZweGBU0zfOpm~O!pxc5Z*0=7sQv=j;mIaJ$}|XeX=yOTR+l`uB}8bz~@-g z&PFzJ!xBT_t{jtiqWSU_j60_0%SdQ?{Z{}yZ%jmhdC9W7w@n&H5nx=iowempqK~fy zn{$cIcOZ9pyE5@v)vWLbaTTa_4H+ncak4fH}RA1 za0<7bGtobRp)epN!bhwBL@_pj{Pp(d11!MJ>!`>#ZP4JAofD;b&DVX6D7hP`M7glv zBP3U9tvccV)}Bge#st92^c<%p$cxuUd{E92NiOit|6QT4)SI4@0EncOZqQk(dJj$p;7+nu2S#oQIYvwXF+S zdQlqiHtW+4c?M^#{RwE@z#I(FZ2@q z7F*KxQp_-uzDt1*MLj!i@%nA4csdjmu(V=i9p_Zhu&jenH2mCC2s|saBPthoHtC{r zT(N!l%UJ@F%0j-CJ`V$Z`ysjl{K-xJq^kV1TVU|TuAB>w=Ru*;i$^KB#GWN92ws8< zU1#hRLA%d5d+HXskTiP4tiOIJ(;ikC%j;Hyg0zyL{@8yH*|i~;*lE4i{fTmG%tRW= z!O(KihWIe#>e21cPaK(p->frU3Qn^lr>5nU4f+ zENM|OOUZcP$JCfhy2C#4YsQwmcD5HX_$dZsp5*qg7`}!n@29gUmX;Oa%=h^~a4SF{ zv>*pTWVh)q|6Bbno-skxe85OJ^)AUxz1@hN^n;WN$xXX7LfyJ4dKvt+YZGzK(K!t& zrxlukaQy!g(M!}8i2Ls3jSNU##1KLnKN=_6*;W{u8Xi0L7X1^Tsikh9z{_v#y$tjV zD`mo}5MWHt(8QW3;x}dc3p#8@rt=Q~nV%fwV|uOsVCk}eGpfcEB>e?1S9TxIVOXx54`wS{1EnE>wiyV9^3a$(>z!xh`2>@ibs^YWV!6%h0=az$64+kgiS zR+!wiTqn2=24Q({4E@|yM9O;cx_CKJSxNF@`KvTS7n zb?;TN!#zaf`Tzl{nxVmVB5>UI{n3NY5=G?kLN32Yd@e_S^~~oFeS*imp-J2pm>bAq0&|XyqAzK9hg~Se zf{vB4x`LA6ny1!^&uQy)`~9cATCPw-&T70<3Y7UNasEzx{(&U(pVWXxiDbd`2$CVm;_Yo^Clxk7SZtCPkpdQKU zYYGR@vev6rTrAm;m{sAi-&XhFXTJY3^^YwF2`GsKNbx;>ER_X>X$9A|Ojg_r*%wOh zXg2yq^){SXWW1IMW}HfQ5<}x1Q1*zJ#0QfagW=Dt1+B&qKDM)n3!rL`u{c^5H#i^9W_qZVi<5=-q2xm3quca9Ta z2baU8>L89T69~akbpG#3Z`-53XVL9Pe#!;R%~{UwwJlvPOtB9X32m_V9)}7RzuS#S zVB62%)gipoX4#44Ea&ud%pHPhX2|OD>>wDB4G5;fFnxC|%eiGZSnMsPb2_Gn`e}BS z>8ZVF$80o0#6pJo17YIg40~^-`Ok(Ekkn@3#n2=kg1|j3DtQ=KlpIPE00b0#gYIvZ*YnkLC za!BU!DFwl}3x@SvidM|ahKhqe+zky_)*cN7_+2oXxGi)XfGEgoDNnONZ%m*-#FyXY zZ{2z#Dzu6vt{_oU@5==fdss{_#{dznBJ@;cfQ8(9&LkI`HlID;;oRu2C^l-y+Hfdt z`iN9jZjd}2=t>GM5~XQ$L>0eh%o*39Yw0D?oJVu8L(-$Z7H0o>_Za|6FzRBb-Hoj( zXEMh723snG2M!|)t`+@)5ky9?&?|dMH4};+`I?5Q%jda5r180{^iJ_PL2ucb>(#aM zHP77<+OXnM(8(vxuVZOA>x++9w(6!MI~f^B9C*e*+n9P8su1N%W$B|7HKAdXAya%o zs(-Sb-VQ@MuskPx7+1_bLfa~swG=RT=YIz&?7WAE{?O{s-90RBiG+wEX%f@y66YgU z0s$j{;ev4E(D3LP4Hb40C{G;-X^>bxtp{&M#dULr6QoQ(9`Cw%fo`KLJ8&BiT4u}x zbFV%BP1l8+T-^FN94V9&qPm^R`3}^~nlX3Ild?vtfDZ*jN25$Keeyl~VA*D}@Z5y0 zUeCUyGmks&?1MCc*=IrwRCeoLQHa=*dWQ5Ht6#&5U3W1OKWDunXyxC8ip^w-3xKwJ z=Z~0;QFlFf$J&{ZBe}51+U(Na!V;3&$I#r>_8tn2Al)xmd7jS zqoiUFc4jdmQD$<{5IO50+ZpiT?`c)2P5Z*+PN~Xof9K?4t+)nj?Sloyan{W92A_X5 zHBt=sjeflnb(9DdioL<6i3J8GLEJr&Co~cZ<@6$g2rL{iiXkX70VtW)x5VeFR9Qt( zTv-GAw(WHh*8VcFx8QQj3C}ElR zs>~=8i?NxccDM&Jnlw2oPm=Y{G*$uY;ThS$AWWhA5*r4_Tustx9b7q5hv_{FJ?g4= z+yA6JNRRU>iA&4mi6alG2N`hWRZjW;q~UQzyQ-=eb_tLg9ci#Qtq4Lh1*O9+xp9@D zS|NTy7}fl8;+y~$pgKD^vf=bDzf#~q znL?W0oOj-BI5*Ga>>aWOw|*lw2nsHgPr9+>~zYY*9#9DYgd^i(b}q=P?^iY zAvo9(!`?odEp@IlQ>un`jFnW;5j?W)=Yj;SNx+0<%`)PvA0!p_M2knsfY4doL8>*n zZ&6tk5L*xZ?`Lw}I6spS)5V0Xm2qs+(iRtZtD=%yrz<-&C#IXs!QItGSBwxVo+Cx3Yw;vxr9Cj{J{Ff{_^ytCEJ6 z2x&!^s)w~%%4e>HHFO{uynzUn?iPd~zxzlWmgK{n1N#CKAg8x{(`}8192J=2P#tc@ zdsN3P*FI}f!}Pf_3)QwQ?`b4eL{P_4mA{zpnFj|3>lVmmXI)9t#*XR`ZGA`uPXjEt z{fse0#)reZXi{HMhPoBxu7~=v(Culo!&8E-U61zm)t8?L^Q+H8to>G)YO#+*rD|g! zNNJ=9zfumh9D78Er5HFOt*XP34RQgYI6QSlwZS=!&Ktlmb!$vqE=E#}g*$crZT{KL z01ZI$zYGrRE47e$e9ntGk{NjwvO7rBk9z2Ed_qnt$&4M1O7vtNYAdSJ8M3? z0i(S3XQ1orVdLw!7n@d%H8BTu4KO|y?)>c9p-zd& zv{pA8jTi9-x9;-0`s2bjN?%iSyIxXck6$NJ3mQ#tb^Oarc|x)u4YBIHkx>+|X`7AE z6)v{yf*RujmD-_BQ~B3nO?yex0)Y2QqWP&YY^Fdz4-Rd4nj8+sx+qpqZ1lXhJ8?;% z1}I}x%vtO9T

@wttkVOgS{i>snGaU?k!>d~EJ28N9>hhz{k4Nr6^-Udp;4xP4ZN zL^Sn{TfJfnwntX&1)i%WSM=Nnaz`T43*dqS7QgMPL@fvv`)RC)L=~+2Wij!jr^Hr! zO#Xi>zcd2uIMdz;V3{CKa&t$HU-cCWcuU5d(i;m2Z-jjWYJHxiea8sR&QkOdOJ-~nyCfZ*>tW#C<*Dbjzz--7V3LPI@hSNWm`CzaAFayw zgcb~NUPh~WewU&al)>slQ(Csl(g&lhkzZFW33k!l9MOUhG|%(v}e zI*S1rvYE|uUEb&rmxbFHz?E$TSr z$A)y!CPu*iHM75%y%(RzC^n7d4cRUMxb;v7eXJKQb`3dFnYmEH=k*c(6f30k=+I)d zT&Y9G%p0vYNoQg`V&PKmg%Nh%h;*=kXb;s{*RPaT@DY+(y%k3P&@5hIhZS{@n+91) zhi$^51d|>+m-H10sE{Dh1m8l2Xj+h~k7M8D#9H4QuyMk`lUD)S%>Td!^{slw?b63l zd_3GRgI*bF4u3yA;DR+Sf}U9Im=Kvf*GC1< zF^7WTi%j2hhb}{q#wnj~I5!$_8EK=q3YWgDb)cfDG^>TGz90yvO8n)xx@p4wP^9ck zX-qJxp>V)==1&inPL@E~?kY23YH8amo-VaF>Z_B128+BwcZDsGGkYow1q~XMzQMcl zc5gO-+5M2&L2iwhU=xxcIVal%Mk`=iaKX~gTiF$%Qv3g7g3E8)-}_uzDjANBplXJ3 zihUn-7KKaM(dP7zr`<{@c5x>9>bi2=NCl(!KM&C#hkZdE3WTw=KBrVJ)IM(1TT`o{ zO#W?H%}6v@T;Ov{;A-MLSz0MwFooP>cJ-biD>LgC(c_BM>=SJqL?TN7 zN|^%Zif9->cqa!p7i_3DoE6_`7+)=$4W65?8DqxLL87+lOc0S-s0B~^&Wjva zb?pIpaJyYUmE`PJJ39bTK(4=F@;4;Saa!NVnIc#6M`ob$}NY_TX%+b81_4|s9hTtG+wR614r4*{h+xIaVbp(PxW`m z4Nel@bNw%V9f$k+kQ&BLcGGwUb)P^ap{;QfsN6Q2s0KZ>DS$JjfmsDV`#)SXPsOsJ zmQKFJpXrxyMSfAOcI8aap=Pf5vBB)d91VBIMWI$&0Wkpy{79Q1e?!Li8GuoXN3q+% zTS*N$F)Qn_w04R8W4N)N_fuS#pXS3QkyQA)~^4R5{hav#104BBtdlBtJb zP3=Q#18K;2WOU!ywB&*p1#`j{X7mKS4@gQf&@An})w>BN-Um2SS$d+uKaxxG!f9kG%K!ov(U;E&!I3I<)bM1+n{d2yLabw%A zWhso$)tADW_)WzL)GEs*vdqabrro6B>T@8mW9Q(2lEtzmVc-UW(#>Hhp#0TS%)2~g z;{G-z9rn1@c$olZ)^qA*EH~gbW|9ysCoug|sk!y}>a~PyJh7MpNP4NnI$Zwr|Ky%W z+$FLiBDBQ=i5Z?&5myaW%$V$-B1{<2*OYOKa9NvO*f=^kxMB9kfUQ`OB;ND~>OXMQ zBI5(=9^ex9+kC>$y_EeV^G}MC&0e0#J?rM)uLO(2mdWIqI|139Uyj1G%k&}x>Fesk ztu3{Q$|w_&>uvH*qsuOLu*(>Gg2-g5Yrq#{$WNtVN9&9NPJw`N)D^Wil9sDF|4$>` zY2UjECr+ZhsRQ#{atQg!a)76=LbeYO8wJ#iO)4ubkMMjwIxOdlGI18gUGM>fa3~L1 zPWI^eS?grgeIraVqL>~53XB)p?WV}DR@x{pDv81E9J77TC|5H~)7I22H_f_tfx$2* zhU90;O^zk}JzLm)RsyTO*%~a$VXaiwF z_z)&CnlyFhfiF;o7wa%pp8dLMZ5=gM5GUUWG7(6I5E*|SM9XHk5 z?=_}n*0~ERcj(uz*#cAh_mBJ>UTxZ%Z-lHy-(qh21~!wPAzdQ!qyS)p-+Y+4i)7`t z2K3<&p%B8Ye$^vhX;pepQwd&kPX)7?aQHBfeJw)Gh3iU%)o=P2>^!t4!D=_aE0}tx zV~bW{N$JxF!qBa!vN2EDYZ*v4$6D9C?RWVZLh6qLKAfK6>6ru$`G{VC2T9x?$R&Nsvex+zmf)eq6}SnOBqJ z9DtS7m`B7-Q>wb!#1mjQX2>OcqEAdnz9$4ad4jWEm*6Yzi4zQZL&4*m1!{_)a2`u8 z$jKm>h$Y+=K~Roc2RhSk-ZUB&sXytP+Wxg{D`=w4wcG5{soG1%}Q9 zj`9nW*)bmLtL31rd`v^0SO~lBc!yXfDvnrHo=#cNA{s0GYiqz3 zz0`f2;!|(XDV8$F#kLf?1BoNLTO$@(kDgK|l~`@B&Z;N_n}MgHrfDkUYEIu3-kXnu z$b**XCH_)XF#Fq(wY-(8qMcY#)t#6a2y5oc0l4X7kQv=bY%rPoOz8NH&`2E$u)MM< zEKP1P+ws58M>Hnhoi2?R__KK64lq_^bYi~GJJ9e>*7Ah2a8GkJtaBhxEMvUXNN_(> zAPV!?N$b3jJ!QJo_u*kg6LfP5Y`g$UHJu?Bp=>!=1`4TW)jeA9Z&&oPBvZ>I)k3Ub zP8Ig1`co21>G3+^o`G$eF{rsamEe1T>>yRo$7^UJdf{sq&b2F3qLdHy4ea)wd}5bB zJESs9yv#RlObM6O8=@_;+paH9d#EMLfQqCJIJ%>^)hA7*CBSUlhId9f0Ky>1C(UHX zc~?|8&k{3X)=}dy;Ff2+s-F};Fqz;&e;6hH9^5b2_LUz;)aI9oZU??EpYY{JW24NX;v1>NX4;@Wn zo%!YYc`>qZYW^Ng1J;$LwyRzI`59>Tuy1fpe1paQTog}G=lW_YAzuD+8zvH}l%O98 zO?)F+cX6~A}9 zX_lnO?m#az8Z5ikDZT79KzHRBg0s-Q`T!PM-t4bkMzUeg8EPS}zCuyHKj!m^KcI!;#@vi6@Plc=wsZF(n!j0ahE+ zi6uR_!B%L=HhU8*nd~j!r%DZeeNkAS-$H_?#i#^Mvh>}m3f?mR=%QPw6UPAT{ZGOi zrPjOtDs&5R!6G4fa{~nytNh&VoE}Re*N~58qfX+Dw-CRzvk$`hwaNfn7Rh3PJ?22V z9KO8|@J+-wWnTy_bw^a>a4{jEAzymmZD3zmAlrj15*er{Yu_w9SH02v`Q@uep!&C` z!co0Ai%OR{jGFi`+0u`kT;8uqVJPqn$~dt}qq*=9+hYBJjyUo*dGRWD<%9dR}SNe2sMbGBWx6?>G>E7_?VD>p4 z(o92ojHZQWXe9QlCR;CrDqv%+@@u;ISIB}ym&-id$bap$csdibKJhyI&UeiYV$rVJ`JRAnwSW(495AVe z3QPY4yS}Fpu7ft83gTH%!`#e7WM+ay(MS43w}dnO4k`kx z3fr*k5&`c5ICd_fgl!I-b-*2hZKdP0dQ+@UWbjy+)rdz*t7D)tF}}+Qty0}#kNT+; zeI!KFgx7E&8`R4FGr-X^xp9vdJ)IDtbkR5%pMm3ASFts$E;eUU;jYc!17eV`ceC`q z-+%@lx{&R<>;;|YWG-i!`~ML$QY&ii_6hP9miw`0mYr@#Y^3Jc;vHtt)4O%G39RxX zApOC^zqQ0)R-L;aK>eI*Iikww2h5wcSL7CFG?$M9{QdW5WC zfy-y(3t)d?$(bbufGFwE70A_u<+&qCvZ8W1y0n|CEGuAice0}ST>HbU&z#kT*D@7V z+rtVZbd1W>qe93b&6^vOQ#R#w2u?w{J^=8pjMuqh8){JK9D&$yup;B*zF|R*_j_f7 zViedqTgZI#N-gonQZaL37(10x-dsEOWed*yjrq+hOZ6gvF4?yi5$YY7m6X|P81Exn zOb(6t1d@Bg4eZ3mb`jh`hqZkzS>PNYf?58PKFhet#abQZrM{?68xc^nI{&~c zap$_2!$trl?>}2uF8JE^y{fMwBO?? zZz+br$6WoVF$G$rEpbQ|9SNVZA{jWhCdkIEws+V-yY6C?U0Y=CA^E@noBqefWAqcY zUxcVB4muabbC$)6F}3Y)HlipH9Z};J6(NW)q|NaCs%`{gSQ_aO*|S3$8Aw+qVfVSZ zm%`4KBucN4FZOM@$KwbzZFl#V?3%!teq^=q_o&VrVQElZSQmwt=C&#j83=le77L> zznOUI3e9nM>O?7^1~IF?O@4%O(`!}+#nIHq+6AmQQ16JzvP^|+jF*uXszR?_tucsf z8NV1rlHLi=3qo_)*J2rKMGKtb>nx3+6=zC~U8td1y=Y=Nj^JCi&>N+6ja|0MOYGI1 zz@;f^5349fkx=e&b&<+DrxzRiI*Yg?t}mg{defJR44?zVZQZUk`AP9QIq;Gr@EA(W z*R#PD|Np_b0kMF){O?lW&VGMg^A<_FH%~LX>tPO`TRh)GW0{xLvg3ZYEGE#=3x*GJ z{EhaOR|0-!deRbe5XsIPfrE*Q8ykb4UYgZ_BJL6EsyrVDIjh=aCU+qmv}es+bkjQ6 zJ1udkYL9NWOT-h-v0nl%@dVcgZ3g?n1um(6=YwAy57ZOE1mfkriN{FriGTTbALX^-5KxrZt`?? zJCYsv-H~h#%v6oeYGU;65e6BU^%lG_bA+;jtpM)KIWx|t%q4I5r$XaQf z{sLwJvt!$>^nm7k*izheBrpN064X+Dr#Fobww@Xdd4fs)5K;&Nhi`627^x5aKk zT;zf>_)i1FdNo91Z~y?Yk3y~_fS)HY!%`HMBfUS`Xe9>RCS?`Zv#1780#k(s)ZM^a zKHe%AGqX2?1lLRSQFvh9S1$4hWiPX)IFP~OY<$LmO1tE)g7Fy?pP*G258Cpd$)GRc zfa`RuB^Y+yzF65)6;xy4q~r7IvK&P|a1F`FL;%s{i*}fkA!m_2MMT=x%M&`CUYY4A_$RJh~)_|asE5)F8A)QfIPK|LvsaA)- z*pzuLa0hna^%XnOO0X|tv$AV<_W7e$H{U@$SeHF0TV5O+$G@NQb5?{q{WiL;v2M?% z^l1#Gow4cPnJX@zAKn^PYWf{tG&UPk*H`3QRkPoOj1eHmDRmb3Zfp3ZyO5bIlA!re zJ4oGN?nQGxEbcbn*msJ=cxqzvxDGb#3y?X;3ra>Xd%Mp}F z*9s6>k-HzZ6m&GuJBn9FMSbw?#Kuxq4I+NiEAiYUqhpV}bv__*`azAbc8mY#sP%w> zY3_OE)O>~SyelGkfCtL3(3^8?s7$r&&j=F^qq5faR+Pa%$#lIZyAT@tnk7bW@ABCM z-4X!DyYK5y8ZmL{P3ns`pcc!M@pfE$seCaKD@U6TCN~Lr)(~~j*KPwDM$^;FL`#1q znGsNns3fsW-Im7m4fQ?%d-ZD>-R?*b@BoGDXc z#S@@6M2a)4*He$~$^p~Kl`Ju%7Y?h(R=fScD7-eKf z`X|d&Dlw^OKQ|mWtuIuvxy=rrb`TzIzT6@)=fe_@LvKr~f?1EP4bPkH!J&TZeQ^6;pnWRLdq1`nYOynNZZqd`#CR zW42exNv7$Kx(E6sP(-Bi&*hvZr6Mpu@ohL^A$)(#$h1h_t5x)6&(FO8``ISnmOm$k zZ$OtHWLX68FKpS|DH?m3N>P`IdSWcL5_M@GFJbp{PY=&^`qP;xL7=jp^Lvq3gUgt=k`c)n^#q8 zy;V&fU>X(>q8$hA8>0$`-p8H72F_`NyLL406zh*L4Mr;qDt_N9sJp3RAZq?x@G}RE z+MGSc$>*o_a9^*Lk2ECqp3yrxV$7wXtPwb7*~zF*rm~zBRIopOA3czQky{d}l_@+^ z(a9Se5HFeB{k=GAJ7Q!0kPg_ zei;S}Y4BM-iP=IgsCOrB$q$W=g)?vdBf&$MA6qV}+hSyMYGyyAm*rj8e(FHxe~nf= z(VT8i{aQsM3_2F@p!bgitZlO*Y=cfno{_aw)|`T$r#omGdhxY4K+d9xN=l>j z`U2(yCU-NN`|X55)@!GB_|#OJbrUhBoWhhcc=j@68|^{$Es)G~N+K9xFgI&X+gyOq zS537gh7G+A;ezgB5d3y*`Tr)ghp`O+xe6RWVY2fRXjR=FnR*^>dWZH6{Lu(}$FCZ> zl73}O=syE=Gso({ZApW-k44&f1(Z992TDJEVn@C*%7U}^{?2f|Ki2QyhVe+yKIPfd z=L1`D8)Jt2@F+IjMblP|SAdFg$GyL=R~DM9>*un{0gtm4{FQnFPfg%_d&$#|71|v$<2tsC5O=xyW(xoKSB4YwHVzA-7_Z+eseHmvK ziMH*NV7YB`KJ*#oOeWRNTyQ^rH7U!@H@aPByOonhNy+@x4lSC}O>FEboHDQQ>_ zK`fp}7c~>)cmPI#wAYsDQA&*jL?s)zEgREqr4;SqXe&5%$L75Y7M(yL?RtegOUlC1 zVIzg-Z&c#BHD#ZL3I)qMdM*S-3}>V-Y4rl5o`Oz(-jC@_{Td9v?)HCtCAXZN_pP(r?%VosDi2)PqDJrwO&KGEI-n2nRbYqK;V1ZH* zHh>U);YV2i&}?FCFpkh~q@a<`N(h@Lxo9`e_ZKiL2JYmOh~xCKb_<1>6lr?4=@V*| z2AhPnKhh|)kTgsgx~{W`p=K&*C6C_M3~TQC8(u8<9kFb%Pj{SBt+3e2;avD1*ZyKC zXho2wYM%$&pbG1YHpx0*Va-6Nql;z2VJS8XD22gSYaoVRXkHGVR&n-*bpVjj_&xwn zqXyWiJ<3jiN(?EU6?T^_y=m>|(C6up;Dm@AHz}!l8jym`kRsSbXc5hf2ID zQ!EJsTCyrEWoFW(zU*04%~GrrUe~j%@!qqV%kvG#v;hge20rF3Y>xhad6O(CnCjiK zN`#wmovbo}!LM8tyPVLWiBbXqEhtIux@R7DS{N>y(zh1PNw~K4LhQ*)#|K8r{i;3$ z=*{mH5bp7Im+KEG=3#E}CRk5wVq=IGLEY47aC(~st4VOiCkWDN>LOM%ry7)!!)vkx zd;5MWJ?RVssOW=jHNn`IMZ@kKyxE;rKG)#DM)*QgA66>p$1Ye60~*LL+e{}$uoMV$ z1*Ss7a%71Jvze&4QqhYs!SUplEO&m~s1rYF`=&P565#QX{q0q-dWp|rH`*%`Lv|sJ zsBNe+%UL8CF9N5NXP2miJL?AARiJskgg2K4VuP;Hhz6{!O2Kg>w&dM>(y$3xLz^xb z@>(tjHEa)Fl#I>0QdJCD#n|Dv*#{k?}={<$sjdb3cy2ZM{U##mPW-{ znd?rp9VV08vw;h-{XMCS^KXMVop8WJHlr9tWsX4^0zKL~5Ljlxh$z_Gc=uLf`j8=> zUsF-5HX!rrwopgbIeGVEGZJabTb+BnL8aIJ9fgrr{-V!k&~{|D+3r`+2M6$U63HC$ zJ(LNrl5}nl_VFjTNaZ95dUruC0HJYclvfqnaBF}H;M$0Cjz~;B6Jd4C!F|}h!;Q=a z*$$v#Qx)=AQgtj%9bkRo&mm%t*|jC9${d{0rQPe<_T^AFO4^422we5fuvnb$`j(H# zXB@U)66F(ReK7bFQ>8?{6Jk)!S&VC{nNHfa8fOs06?kUN8z4}+DOf%XIXj`aa*#eUzH zKGp&9WIRe2H|$OH6R9(oWDb)@=Npi3X-2}A5QGif5vskw z_Gg0Lk%a(FOz7x+n-rD=AdIHduKX;}25X-!|03M{g0X=+>#9q1eKNOU95CULg5tl-gslCZs=* zoy+2f(xIZ!2x+ef^@j8_CIY5lyo1s%pI%rmr^~o{&$???+%Y_7jX(!4r0M!ywy9u- zJG8SG4#HYQyNC%6VE`&%U^Dw|#TBtyuolth>(CGW3-Sk#rAA{9bd+y3+2To)*4Hdx zTo{t{88@@D)UzwSt&V05d|@<@^J^vn1PUp3Zxw{yJ$-1{Ze))(*1Y$Xciu`WupD_O zXAZA~E@OORoKIk77kI+?*+PY}Mm6^>QiKPPqnw1}7{DDG1P1{VsoUbBOV{YfLpm#f z^dzr92+**DFOU^!%nnP7(!Z@!HI#gk3B1e-#L<}FQy>T~8ZtAbunN1Z4I+?JuxI!( z^z4~a+5JTJ(?VNhUhMj!vh20@)mg1NdP4ebdM6VNc~fL}(ad+_%S;pMR_KXqgmi0% zj3<_|EBLoj9>WH7aGXIK@4F2!mWyY|OD7oW_LONjbOQpb&vYB|g$kZMe3I#}MnnWaozvvWpv!|{G}4suaMx+evBWQ%e#MWH zw%eHYjK?MP*O|^IfsK$A^ zC>e8!qhhg!r-f%+jXd*;bT6el$(IzE?I2C?4ypB2l)*RQ zHvr)ld$-fVyo42cyZ`wv%2;ByuWicHYs)+_F8$?fu<*~N`_{-?F?Mu&pOPPv_{4Pm z45(oIy4pSz#-`@z>y)euXM~A(T7Nmz_XozW7R%R)hoyQ;u`DjCTaFFH;Gbe;SX=cLp&bBl*t@R_v2kZv|`8AiC2;|(}YmUGp8~hHp zAjlIR7%pmew6nW(S7((s!x;oaFD+aEJrb%Wf|;|Eo|f)*@0G4F>LO%N6IuH{|*G9eaYnhY7fB7$bt=clRnA zS-T`eXA0B!&r>uNAk#w#Qs6_u#JSGS_m7TAI#0x8QTa?!yj*XNzKa0nlT6zq5KLtc z9ROffZqtX!+3%(Gj9<@-P2Vi>dfCM1 zoF6ivUQAQX6}61&RY3m8$#d{pNh^@kBnmI&Ugv6^m z2h5$aD8Ew>Afb()MqzIe6&eSQ8Y5jS0f7(~amsrH_ohyHP;y&$3=z-gW&qVzk6K46 z%2Wzy#uc{b17{2HaGP$4v*ljFP+O3jD`K0PTSx#pjsKydj8Erpxj*{d7CmM4L}h-L7WU^7j-2y!1|YyEA;xoxP)BfrDF^L6WZW@@BeiKEJS zfhzxF6qrptqo#Mm8k|DpLoes{>oQyAi8&C`E(DYow8s7&URhT8w6hobbr=PuF@Vf{ z#f|Jsat{*3q>@@Soo#NY!&*ND`h5(H!S!(me05qVd&Wb?b*cw2a)WG_$zt(N9!!oc z_1C$1okToFBZgP{s5G?U>>{{P>Cg0`fd+VQO^-1~e}G@!$e`%hL%y35!$(TLyi*+k z1j)}&X~SL^rYsJ#fZE#!00CvMbVa&+YG3pftT#J!kN2?FcthVkn`jnSi%FmE6e(vh z1XsTE* zXM{J5$f0MS4|t?REE?4}oMTY(>_KEUQ;|6X?&FH7gSd{W*N2!+OsQ6cjVWoAi z1}WUC4zII}`eow9g-s@s4|CAD{GafSQGPE+3Jp;UMbg7Vb$fiFQ(!XQLex@YH4Xzj z;lkD;t^II^E5Kj$^ncOdYFNfP(%2P{<|(<{ToXUo?e2KV-#}Re+vpBF^te@;#7yF zOn}SbQl0PPDBGIX-G-Z1C$yl}Is)Met8v+JwS|t=wY&fePe#5{Y2#hy2J@7Om=T4ZGRrGs1*r5_Blj zmkUrUxc1&uz}LPj2o$bJHMgO_M>SFOCseJh+d#e$GwGPm;JR_GmY2uQXTiXD-3@fK zPe2vM5Sx{10uHHh&v8N&Y$=4F^|DvWh*x3HXRSqf`2^>B>?nUkwDJ%Ow^YPPGwbgx zr_|xPBlHpNwv;pVnI}k})ThHh`QX7=sz+^s-LEke{F3fhewIFsJg2Y}&?$o8*UeR^ zmjjUAj+DkMyaa}MQujY@jc?wv*!;ZK{yypg=1{>z=i%KQ3FZG{2kZOiqS`U%?(X0K5&xE;0!JzFIO--QZ##!P6}Jvg^j-xQyjs@~Xxy$(NxW8A&VD4fOM=1AA{c+K8v2C`VnE8B z!)8yr^_=NH@k4c+J=^fe7M0`Glgaf|XqFTN#)#hmbMn@QI&BzDE| zn6jRnnU2x}?);~cdYC^nkA(N7v8eYTX(1(=DD=|zUMXLAN1$U0Pooj=pL~cxSO0H4 z-QMdAlm4mD->-v0fi!Kzgv%Tzqv)v+M9}QB$h3j_m*TC9uTE zxd#T^E=1}OQO4<*SPs@-nW12|qK1D7bO7`ciF1Z^I&Bg8ABy#&+fFg)T9_l~=E9at z!E=Cz|9$d1Lc+Vwsne620>6W1+le^GM5lgN0Flb(G^W;~$Ex0o)Gr;(sJ4HndG~4h z$cs=bxYyX)KV)Y(R92ke`UhI#1Ty$m&opC}U2mY{@I^LuH%HyCM%%zdWc`?}E)!Im z=|pGpC1Z?0sye*&Q&ljQ&X11Sq-?;BN0aQoI+`{|B>ccDNtz~g--}Wnx+-mUT1O%| z9aBq#Z=k8}KGMD3{E%;ONUHAV7YBM zS4|axShoo|s!>fhh;eEeXPO5wxvqXR9A(@Gi$WiOlRB z^}i;!G5I)JRX5m_u9Z3|AQd2LQdRvr0F%ij4s8w&l@&i;V5w{Vw^~QXZoCO+kqCL0 zS3l3k*Ebllujdkf^9D{*yJ?mO_E=54j5uc~N6rvwLbjHrCRp6VJ`AQc^_9|O^b6=m#?sxa8&pxCUaB2)70_2O`R{5YP|=*pdq%_?Sj!0mw6Bw`D_-XZ(l z$SSO5o58vUZ2PCM?iWJZqMp7!2cDny+qv~x=ON=Vi0cB552`?)SCYQ@6C9Ef5mo5! zffAx-+!n0)-#?HAE7i(M0Yr*K!GgU0Ey-AT{sou#d3wu)3W;axayGHka{4c4VB|i!A;AW|2)C)2=vjU@7(3jR$(w z=I1MV7@sf45B}rSdumN+tAHCPJuTvf;sNpF7#8|Y1Kk|fKo6DAS@2DjJWZ7T(DI2z zJIBbl))jfkv$_#z*nOMK5TL}p(H1$$f^<~DJK8)ao`0uVZq}aP+~#Af3yuCr;)o^R zsG3z$rCRaw%Ov^-gy)E7Cm$Szhm&;q#+_Wi6A)hCNg=LG%C z^HvLIkY?bxUCy@nMD`S&A{$e1cA_M+bW%)Vtr;$QGs^*ND9&Fdmh?++=qd1%zoJ{2 z@rWe(o)OZ=0thW7u-r#>b@qmqX6<9~YSiwF^QP{=#9>&B5ag5*Y?3dL<=;sN#xzr0 zkwRn4Ytr*-p2480XE0X3?nry2(VprDV$O4i)E`)VcJj{B|WK}8p21_ zV+7RQ@{<#X4EU6|xcfn<8ve~K(W+@^&z)`nTpwd^Faj+ktvc#ZH(an2*XP2K9UY~E zg+k(sw(f1N{gAv5)^&2C)}=z^>lt*`(UV%XVf?*k{osvAG!2gR`v%1BWa<%wya_^O z^5nq~dJ|uFIN$phP#Mce{Oz>w>qFAY*{;iijigbQY4+1b~^egX-W;HhjRdXOTdli7*CV;k*d_Vf#_i$^hO_~sTxY}*gWtqv_fwa zf?2(}b-K0g7-(}D$2%Y~M|luTEr%oJQoN+CK4A83spX;PdrvG$nvrec1CH$c1dr0% zb1cj5O?h)l)elW5at{td>f+1`i3XnDb}#f~MK#RCx=ipEsYMg_aDC9$()l~p=Rv5F ze|0pu2%(%87rpfBJl|SQxn|AfS(`?Tr*6>|bqj4Tvb1OhNzD3jxgb5Rkrv7N8Bu&vsxRzu#^wNm ztJ4H`nBQqw!XjNFfS5HkAAmp%YT5gLlXI5vFmu+8AkByGFdG*^6U$2YY=C~rQy;sgj|{A%{W zDY3a`%lT(NXmAl;>ID+cQK#yL(S_|YJ?>iDJ$dtP;jrTC!&sB)^aAoRH7Jf-uD^I zI(q&O?sCJ=x~yM3ZCXu{wQ0s=MV1AW{w|-|nG)a6vi!!(n4JOFq@kj9=b?mT!y@qb zUQ}g6njuFDvj}CLqp8Bjm?M6DlI*CCW}*yGr&L)>DhOZyZ;+&96-~O;Uf22AO^r>P zA3w~C<-v53SS(Dnu}SnT{RV5x`h*Loz*n!SxsPLtj%u(-2jh4QOMWEwYG=4ru?EUs z6{I+@y7LGTe{me8Cs|qA-p#zKmS4bh}o<_{R$=_YGZlMwRwg z5LLq$h2ySZQ($?v$2;;&)1hQ$bxtO2RL?Ufyqx3@vPn514P{zI`S~t0Pdb9dgfuu! z;)wG7k&*S2c9z)v!%=v%LQSy2DP?I)ynzDgVF<>CW?K z)!rBa-s%ejY7tDdiER0G!74uFw;fcEs6#;fto|WWuRn@?{kGLp^6fp49rXdYi^yGivcmQ$F8#!o&FQF#QKP zn`+4nWB29v&-BOI9CQ5aP_+ce$k9ePA&?XccCV*}W7Ro`A|-ySQT;5rq?|G>PjyL> z&jYv=c4eOJ>Ih9I0O4f0ra`)*4M+FQux?9~mc%S{uROX3j}0}8 znynHUy}|5GfJJtu;5Lb&VlbJ{+ta_~_vV3WQ+iP;`EZ{B;IYbhqWNBL(!wfWPYayX zSw<{aU*PtJze>s6D{Yq32%L&x<6#WnE1A@au5?(p+UU#dy`158S8SN zs+d(*a~Azkt8Ox2vpKdK=5%|U*OOe-;bh(1U5F=gtQ=oBhQL=un(n;ZdP`>8qK3Du&JXz4Cm`EiWcLkmA?;066BT_D{;R)@uZuh7Q^oc}rvfa>wT zVM5VSvLgyh&KKnuyUEe_Rb&+@ldE3Gf=lUN1RMDc2<8u~&!GuJl9-d@yL--5n5a8` zpo2O=u~0~RraideM3K-6V&8eq+=y$;MN@#+i@mneG{DAE+6QOVE^mO8pfO~vP@<^c z15YJ;n}gkv^t4z!;$Lijs@P7UZ|o~tS+j*l(pD(_4>FCxeH%~Rra6(mVr)n`0&Ni9 zjTpL_7O!pCuyJQ&cQ7=OgycioDyQ7ml;HZev^EA>?Z?cZnW!BB_0nuBt!9UP<_T z1hqTYI73Rw!F<}6T&=wA0BoX~B`kE8cP?Mjjw>h1e2&{@UVn~tiAAChkVMG4N(_5H z>vyYPxAthUSsAWJ2^ybNrfKh1FM;5HPe*^sv6tO8D@O1{K~oBz_}DU?aui}S)FI6HbysdqhBEQoPkR5B6}QWLJ87sC7_Rd82v>T>!Si4KMKn3ioIPc~ zHfQXfCxwJK7%!^z&G^71I15K`e}Aur(sVG_4;wHW|Ct z#0zHFEl+@re29(bq*R8f7G4T{9qtP^$v)dzA=o>qzqq?B)QYK%7q!rikn2TtKqV&% zu^qJ&0V5avvfXhlX+=WET-#wQLVGl(Fs#k{c5R*0O~@OjcvypT*JN;QOO^dKz5qo=;o246*SKbI)ZC|?oT$FmAjEtRdLwBo+@>Qnm$8>s~ zuw=wV=>RB}bd3nG*opI?9%`$dicWv5Ffas3-Uf523qI*g=`;nM+w#)mmI)U=j@XN_5%D zJWB#MX_w9sj6!Mo(3K~x%zcRvqwvga(ai`5=CR#ae}fBS0gc2t_n)9UTPlbpR!4wK zQ)3mBim?&^BuOlmMQ`Mlm;1%%nB%%NA=f6SKo;>;aQK5TH*{!+c)ls ziZ5x2VC?_gBOLRTub=ssxC{q#x!hjnm1BF6d{c~a*?Mc|$&^|Ep#75gKCp2d#3)Zr zg*-5zj;#(b@pp@-y2E8z!Q%bzxZF}qQ<`Y8!}~^YdUz)^_4K|ZS}!OTW$3IbS>;fq zDQ`hTxWKKY@ZF`j)F3K}yNNeMFiE%}OeK|ei868f!?l zkzSs|1W?hd4_M#;s@(LrjIxAR74Qda!{aIi;^B^H7uR7>-C#g9inT*IiIzW_!U`pl z0(>TbcwV zgRPEu+7Z)BUujX6L%J$0JI9KQWx(8U&fbb^97nfyT;k#h@;|A_{e`rl3P5zU)O%rb z7y7RJ>;2Q-;^ER0Z7E>sC;agZQ&k`NePyYOvpl;@x!s%~4szVcaO=&rlQ3XS{WiZJ zYDYcVV!;_3lg`6eg+VDhBrlvkUzPWxq+)#S(`rfCYWSFsUE^R*Vi%Y)gC)wv4wN0k+Tw9qHj6ZLpF2htzl5t!2zWr0xn0o8~A zg>)efSrV(1F|=OGmpREBSY{l~*^+^B)!WXlyl7O5}=p z)bfW%@xjuL+|*hryP6${X-Ea|J-fxW`JkJ*&t0#GG zBGTo$%kbjY;Xlt!gnB+o8^O9Vps71Zr$4Va(mfWoocI$r#|--&amJGimuxpvd?Jf- zKUu+`29VoPyA;(P>!Yck<}j5`z%rInf&rpdZ-(%)Rz9j$1=*(%L3Ue;t4}1l;z+}H za;qe57$VTrjWnUxDM^{9be_a9HtuK7J@&an1zODR7#OPt8DN{NsWPY|jJ>6GHfQ|o zjxf{_biL!lT0^LnLpLDQYS27+bEjt$m0#Cm2z}T?5U`EC)gx#3ENCs`;5r#RPy`%} zzoJK9&Zqcw+WLHdS(6xcCx*vYB||)eVpdVbFw~yLHPPXzj?>ZZP=_swFX&U%$ww^pTI&-B8}V@~qj+x-$UwTs z?#B==MH!3B83-S9w7@(hQ>0Kjz^k7MR!IscH$!WnU$;p7p`&283-_m}IT$j3A!%g)jNmiMoIONtGifRB&M{i?R zHP6KlYwC@wSgsU3tFR|bAf6jC2wQ*1*(P7nG|hB^s-z9N)#(v6i{=4JAM<`1eU5ui zg{9rezbBof(3B=wvZyC3!sVSsRQa0{kMNmm`#RuVWh?Jbx!a<1ut`(WYoPoo-9v#y z^z#>~UK6)7xL+o6rUxp9X!Lw?2dnSN@H#2lT=HTFn{hkFN=}$SY=ciQD!qN+x9&27 zJ?MIQFq^LYo0qBnyNU{|BJ6Fhm4#qul+T8U;ra+$ocSLRG507oy6M*AR+zD8z)S@L z^Rx+1l>k7d#5`0;M@e^-MrnWPP;KKz#cC&M3*P$mi0x4{Ri9F?v_nMqv%_)3Ay$_f zyAk#oHsGPzkv2F+M26o`sqI%B=N^iF6W;oEWUmofe6Y&%waCROQH&SeOJwu*h35tT zx7qLbnsmZvbR5R*w&9>mfswNHBj%he~k8>G4*ZU6P<;|Sh0I^ z6c(dZ2|gDYl`C{=slKvW`laPiNpGAZy)U3v4i!+C-k?{9Q8@WGVb>5CA6EEYKIhmw z`F3e(r0rWWeHEFkU}X@k|HWj=eHA`ngY^+IA_Y7CsXEwbzP2>egOO{T+uBJwz*|+# zTFy{-g%KRp|I=kOj~_D^Fr($T1tR!4p4Y0TGsy&S96_6}!t*9Iqyf=tcDO1+&VAtL#S{)B7;@=e2CZp z3ym~niYV(A{9dpv*|WdSB8MF_lx_*+_$76kI>}0C@vA)b1a%*Xs=O=b*mD60bY(7( zvxy-jJ$X(gFJEF<#vEet-=dFMp<1Lr+)RrQ2D1;8WYQv);9N%&Gl9-DyXoWk-8;!L z8J%ZszH%7!x_JpLYBe*1Z@(?P{DL2ziUTcU6&y~vKn3lndydmQ4aSmmPQM1OGn=)Za{}tWfu1d2Eyd2TjzSwS?RB6uwFjLAz5}g%% z7K8dFq_UXt;qXYlJcgCD_RRVB0rIe;{1)P{^*u_Dfw)bd7^$3gf(X{tAHlYzl z7#pgb&&thbL?7Lg8nI z3jJfGIg{UVf%q}ZREdYM&z4H4R|y5jW-F+_xJEV+`3vm3va3t83%e9&t&n*vK^zy$ zif&k;ZYQ}y#d8@mwyE7B9b%&8eUczCIJHLhRepmZQbsAl2>x@#FG1&#pgGaJS>&GO#P%6G|91ZoV7f9mul9*+pcy20t0o8 z`4Gu^uIN7rVlocrPhQ8iGcX3zhuJr7f_4Cw42-TWv&8sYR};KB!4pS>H16zYo8nC< zo{!dw3jD?=Il@oMDI&qJg{=xeSPh~ovkEMf{Fj4*CQkzP5LO@6sh z*|PLqt4eax)0%}$&&GGZ!7ix~uZJH7)M@}~$hA}X^Z-LdIM45pbw1D2B>*z$&$O;= z6mnl{rsAA||A;g7aD^kl0S~*>9TkHW{Dg!N-P=Zti=ox%H(bo)o}FnKS=HVbi(9c`lZ=6 zH!*tY?OXcoA6)^p{`LZnL*o;+kSZ zn8+u@%4u9}pZ_{x19_RdFcy%_K(IYWE8(6qkrkKf6M&Vnh9LajWt+dG7@kBu+b{nd zDU7QGC;Jv^O5EdCGOQZ+>qvm#D8mVW0(eh< z6)A_8)28M5rS#LWTP^Na*ry^bZL4DC##OJKA-`YKnC4(QE}W1F42%`{^I0)wc1Put zEGU}I7(FA>j|%YEfq)2FYs=OH*t8m12>7Hbo(~&03b%D3%@x+6xeEq71K0!_ z*u18asqoEGy(C_He_4?4CvTKs{Pt-_=7^ENEROTrXE<9rSdP( z9t!^bQRB2a;MUwOcj}zo0=D}jZ*4c zRKVp84V>`R;o(qHKULR1|6NQ)|2cxd{=NCB4F06S1M1v9KqpS>{$e9zhztsR{1W(0 z+?yRECmzVIO^}p0e6gS<-tV3-pw<*~G_5jUt!Gl@0PSjl0ODFV$;m=>76(cN#L1vr zDMiQLmI$=TH;*7G^Fv(u<*-F3fW0>61H_Ky!!OTnQmc4P^-b)ZUtC0yB^9ab=Qy9P zcBHh0`!E59>qtF#M;DY?18~0PiQ_nloKD9u6ZXrtu^C`gQErkPDXSv0-XQ*{guB#?r9f!t? zHS-?k0`UB=RmI8@0CdX1W^M8kCuJ#g(KDV>6z8wWN}6mt5*hwr9CI{{-&qv{_5KU=d+S+c$}zjIAPmTc_Eyd3{?l#ech8xr=`7WNvKI3VtMGtK;++6KgXkMY8ySbcfaksv97lr^UKnw$z>KaU1|x!EG#-a(7m2<-<1OQ`!+Y#aomp&j@L2Y>qR};TRCLfUiDZU&--zo~>z>jWq;? z8&^&a77%bXQq~`0V5*q=Lt8ihA4gW+LxpP)+7|9+JvEbhtUm&JF|k+X{~Ve|vh*NVLBEoDD1>JZ@+ZHSeM{!U4b34$n&`hIAQa(Rwl) zM0$2_dV%0JaZY5EQOe2TXg2A(b7`(L5)PD?PW(eV(S?RyHdF2vLGjJ~2HYDW^F9@v zQn2FQVt6SR^-s;JLO=j&00M7ZEh)}^_N7QmFy?*uxk&bTvuWASW5J)o9n+2IBB*^y zylEr(9pZt{+@riX=@0JNxYrFWs#gybxCB;qGYc&eX?$t-E65$Zc46SXe%Ij&wSnF* zZ*|yI$mtF*GBa?LBY@!wfOt;7I2o5!#MM`oUM#CZdxZ#Ob1!a>+>n*ap?#@MtJzNSY z`h>KowSnmz^hG94whXo>=e2DSZ!uUI7M` zfXp(eGnPw+5KtAAYPGq$&L@oRZbg<(i~iKQ1x9zUD{Y#CpRB(@=Rn2pHqk{PZc#RS z=hOV6ju>ukvPiDIOr9??WK6XO@QKWNuEM3;y!j=Ytj5Z;_w9R!Lmw}F#MXD{o`AAtR#J=)XYSdP0$ZGuX%ndJEAV9423rztyv!@2&=2%3EW- zh)9y}ep*eG44F9lM*S`v=EPNLa78$vY9rwx20A^N!b)yT$M`-WAKl71Y?pml)b^pX zRtPelvWLrl5zi6wPg>A^N#Piz2T;hQh7V-nG;H++J0Sd5s zt%-J<*A$P7?921CqUY7qMbJB%g`vbQ$JGz5ei?g-OZ+Bdg>t+&J5L4g(w`gPb>Rt{;bjcc^eu`1=i z48K|ETYtl=0kD@B*p!lW+}qul%d5xDZKbs!lhx$vw!yZQ=3i(A;i+QR*{7b$t2`b! zFS=9tMoIER2JYYPp03@m{N)Wf5DB-)Sc^*+cpvhmm7#pMPCE@CfXWMay)MGm>Mb?$ zBt&neG6U4Cx{QD(WyYbJTnzwhV0sm63FzYV*$5aU2l(3lYIejjOr7vSg&Q7JPyq^5-v<=lxVhxdM`XII_N{j198(IybHwnqA1M;|qQ1s@AaPg*_gp|S^tFr200z+n&Rix{xR+kK1gOR zykRP*5bCE2*B}i*%wP>_G!1sQ*dJvO^3C?))byS>^PK74xE>2 zUILg>bX9OA!r)`Nt6?AOE!Km789<_TI+%tHzrEU zZ#T0I$?z&+0ol?YNs_TDjat?tl1`#u4D9pjh!w@CC>hD>N(kbO-=>S8yohG&9qg%F zhpbW({PZ(8&m=i2hX0QGIStc-3ONPxKA+ie8VHC;=U)Q9$(MzT5VP-Nus|4nS8^Y& zCi0#H^wRep(4$Zf;ronDO4B!iQr|-&m2=$RCU5a-4dc!Bv6WN=PNB16T0mgNx_Fdn z+uB%e3m`fPGIf(N;FFW@iu6gotoB@1=eCkj_sdR z#$&*dY4Kl!&J({o83?o#I?FW-Fi*bMCDto${Wg`|VehyR#!u9-sX&4S?m*|=8#|W6 z3tKrBE!yi5N7C#))$!#w|p_=|=PG~aIG7T;cnuvE{*T2p=5O4&5xzFVLM4*f>=FX$Nm zb!X_s?l_?II%#>s{ULYD2C_3uYd(W=VdKFK950#?#5HygKzy|cP%&W{U6lhfDO8_l zHf#RK^r#!u9J|_pgpq}AJQ?22FwDFnq8Konm5pY+tMQI@AWy|(y=~-J8zQE;v=z?Z z|CM9E<35VP2d{StjaaiP#T5ch(rYUq^P4;^Qrk6T%s3E~9Oth5aBPi!8g&qTt#SwE zb}s_!C)^*s0HbC@j&zle=Jp&drHW$@fiHXb^fj_%nUxJ)j( zL9LTeb52spi3)8KLF?!;G$aonn;94@yGtEj z#@6t4udu3fF=qJ0D-ISJg}zwVH-F0TgESU!gntjA;Z}b(65tR^HWd9?kc9zgkiPSVL)3k|$ zLw<&2LuL?Iy|6haUI7*8tLHxCs%%xHGll>cQ_Zk#3S0h7Rfp-k2aF+9^9t~%Z|ANfAnK>_J?F%931_OFLQeAa%GyCkv?>Y-l5ExV@A63JW(&As z@}}pX!7M|Au;|tMd1EIC@yG<)p&~wM4`;J*;V9}5C8jONIaN_B9qzTB_{}q@22Dm9 zP3c21U)mN2uFwfj&FUPrRAnv#4yJuU!lrwR#_3a;n?4|t?TSMc-Vb^5GR?TLu~r9g zNAOjtu(b{ajYbG`&$E~`%^KU1v5u$en%N~O zwxnQHE>|#k*VVQqO@7tyHPybodMVG8p#Os9 zI2Bdo@z32LVO_FZz$)oE?dzNnIRPprQBaGv=(*KoILR|aEuM>pYzI;bwj=Zm^f0?E z-_P`IJcxpZ*X}(_pZ9#^1r+9~+YrbX{L+lGoxT&XB$OX80+5&;%z)HxvnVY}a6FS- zw12AjVMCU!=b-p=Uw9afk&#-kSJ)qGNh3J|claPICk+dEicQ!akmf0y;fdJ5??xbI*J4RWJ04#sXEy*{2(Qv4CrR&~0p1Uf*KD zoz|_kj9|Au2B!7;Mg@H6MJdwx;g3IDIwPhSBBStZVJM=sBSwr~=GbH9+~;=nOx1cY zS<8_rRB%ct{}PJ3{Dv=j=o@7bvQwo)M02sKCOe!d~ zxStKh)1; zN#kbiM^?dyajRrwm}8*&&`Ts1v-IJ9fLKh!dZ-u(3)18GXWn~*zh(d|zyEu}*WO|% z@@rJ$CWrL7_S!EiHS~+`er9%2)F7qH3CW_KLq%ioFiKiJGFd|!)d-Czrzfg6my>RW zgxZVRO)()c#TLryk8n-}1_#4E;jAUe!ZPxlg2YoZx&m-W^D4Oejz~)}(Bmq_GFxwK zEl3u9Q#YDROHhyC1*@f{_JW4IT@49?Nn5Pgy7F72XAp^_U3?4obo5`%BflYKn7XpD z=rE26+lbAmet5l26+AI$C8owrnMKYa6&-~&85!9Vk`uHk-;~}&96#uWdXo#s!TV$C z9b5b~*tKkAR`W<3ITbs^x+l>R0=7?a1o&6&CHBr3y`OI#_oqQm>UH!3BR60kfZ1ZT z+wW33sO3t;t|-Z%oB62!j-tXxCS!MTZ%bzv+hS}`(_$_U6;8AL`$gpz5EC3Tx$qtbaW3WZD)t5bcZynTmGo#Yf-O0mI zJU&PgHFh`FqIQ@h(e14II~|ZC%mx?@=F&YO+deZ@$~48a$#;?Dd?Ps`)AKY86!`Rs z%{TeoG>m#COpsKYpu^ns)o^A}Q~B+p-uxZ4mNT4VID6+?Od8EqpF=(eGgAHhusoXt zksM;mNPy3!Gaq2qjSgWmXqTkXZ`wBA0fT`G!BK4G_@o*a=B0-NsH%XPVoliVc?$RU zQ*{KsYv97Z-1p2C?zg?ECds58D7 zq8Tp}ZvRKOdpg{W1^)T^5Ed!m=jwANSE)AbtrNO8#{>0<@5!LC4>VCOe{T(hfeuV- zFJ&j$u?#9%UK%;bsh%%lgW`XihQxvu+0vj$8~jPlMn&nNmJVddlEsA-jGZ-Oyfodl zz6cHACaLiF^r&YKDAY3*PO1mWHv=iJ_Jv%sfwsFRK$Pzgx6KA8(m;a7q%>N8ggeDH z2{xy`W{yQE7ED3_7MMFttRMw@erE_q$lH$Z6^}c0x);xZT1~CWF$wnM8Jn%2b0rm* z5mz@)2;)R6&?97Z1B_bed-53cOAge{@X03XDFIgSNHiiwd^59R;gFa;RW)X}27BCr zKuCr&D7qS$Al&Ls+>^DiNL;mt45(VU?q!l7n=2OCkt*oNs79{I5+Qe^0kA|@u7&CT z@*k>5I@lP)0S#VjpcFOH&!K?%UB4`5HDC?;7TulmY(7z&Kc36nkP@IaAY}~z5qf#o zS~x+aSg}{^0!)MQ`Ar71*xpK46d)k43t`&D=QqIEu}CHo5rSq6PCuQRsx3HD3|?K3 zvfjZ~eiFVI*xVX@19JgT7p+Cd-xHeKy1swURDc_*+A*HumY+mvaea3IIs20n(g$9tv6LU>FkXYSnRfcq?j>e$?g?RuVM8R>Z z)}A~rGMJyE-}lwZ8cgl$VuZfsi_bCSp+hA{B5f)V>p59~VgV@o8pbgAE*6`05QCG~ zMxMTk)!Kj@cL)w2X8!rHqP3hu^vT8b%IeVpEpz3qbo!DyA17~d`2~7tb*i(Y*&!Q? z!1Gx>V_XB%$4>M#*HyEPHkJH7q-ftF<@SB5sjmd}{)jboJKTW98DS7Ofw!y_<0u8n zZnzQS_Pi>lf6*nBA12f0)apMkXVAh2(?d2!IaOr2`69ni)egSt0y?5KGd3x{1w`btQiiduHlyOZ8`4HgxWJmkl ztESaQ2%Z$!!sCnFinVWTm5Q{r)wd(ubwr#^-Iay*JE0I`4FQhE=K30yd~4+ za*ci^A7qxvYv1!k8aX=k5&Xa*CxOfdg=_GqH*vSy7SGnt+S~7>a%wwVyTU>Yt5QR` zMUo=ID^wNvL`KhkQuJHC9d}(rGfi*&lYZyel7t$?O-1X)swke@oLRKsYzn z{~xdN>DOowP3g2?-|V-{F}3hqpFc{{%-1d+*LF9TA7A$KR9UDs)5{#)(O>>m(2>II zUPVV}7h3gE;?imssusb812g+)AxsrJuK{_-7q7*177T|g5V;j0g|@>^o9NEV(otJ; zx=Pw{b5&b(Jp41n+68v=aD~g2cSU1o{-EcJKl6!tTr5x##flSUI)|W7gt`bgBMh;} zJ5!8f1H5k&$MsRD|FMV}g`h77uqw7%j(73UMpZuEHgIYWslYEhq7a%L`9<$_9;(HjO#mY}&x%G8MjaBk+9k1^j&Rh4YcUEgR2_YUu%Ah%5rD!N-Ojp#2c`*B+^qbWv*hucnUh6+i;56gMW&V z{~#iKXVJeW18B(~7mBE`o-gYSnW5Pyk|QN5hNVUKO#W?cbyL7rjzQymxXHclMrsnU z)D!I{ZIVvmSu=MVIGgd1PH&(NLWXD zM_UAN_x{AaZSf@y(m~fD^LunXw9)%(!pDqEQn%nv3mQo7D{Fn8VQ;H23`pY6*}-pF z@^1zs4i>>%*|QYRa3v|&e{oV&=e{(aoEp>hFk`=yUdWwG0R-~2((C3z+trDkqwh-F z`%EP)=?d&@`z)|>E1LM|vzlf6SS9nHhhhU-;56R*h5}Kgqbqq}CCWkM978Qv%qGUE zFAMT_^cUn7z+v)#@kUPta;wsI?_7ycVNO6cD(133A~@oO*Rj4t{gu%{vPXX92{;2P zt5i)MgF`ySc|?ezy0h00k7nLI)FXtb!SE!3l1^tO81PoS-no$xk1;3;#P`?lnY$s( zxCNUbL#Ru0Xl-yU!1&MU2vf6V@LCYqaG4T#WHONQC-WE}<2X^Syt{@ihi7br-CgD5!qbs|!B3#At!&hEH! zdjaTFm6^IqkleJu@*P6PDM+LcGvY_}5X>Jtkz5Nu5tMh5(%<_)f64$;MB(0JzVO{Q zdKN)Qsl&ApsDXq=mfHzAIKs20?JWIRF34I}-g~rHr6mv@MQlnqxU)zssJUW|36Kc| zFl%!==B&86!H@RMfQYAc{E8R{HtMEun}1JOhOFfgjq|yRko+){*B-EYNE)zQ32??P z5%yVJMqZmfv=@hS;>X*QNs%h|Trc=x>sU zwh?(hWl`Nd3C^jTr^pt<`xbq(jYRyNGtBV0I~Y}tgjF%qS1G-NOB$z=v@Bp<#lbW zM17Y)_C_KXl@V%-mu$SY^PoLB{SUYi>Eu)55S!Y;0N!zxd4w!7Pb;9HMUkvp4H3AN z^gz)DP5iZID^qJ{loAT#-cIKwgp;)sCdiinTpA0W<6Gq!s=osv)5u3}CUghH8z1Q*&ome~bZN*?1)|0KyLOO0-d)^B+n~Hw z4xv>sm6AEAWrP5zOL27JmQ?Wn1sJgG5R&RKav1A&kiK7bQ)~RD4QjO^@4mF}9xTgO zZj_cGJEYF%_jp*GD>?B3Iy?N>wa~!4F368N>uQg5H98{4s-0MPad?f~6zA!ctyIyD`a#Tc;p_qK)&H;;*W)`b%>DnxYs|Syma^ z)D_6XC_(l82ov}!|3l{>(wfekJ;~JYuS^x6P9eypVAN;D6l6FHzpG?t1{RJaNbMsx za}%mt;0e~7BZJE#aOh}_Fm1KKV3_HSi1CMM!1S^dx_lkgz(C(YB?|pah`InN_Uofz zp5><;9XRhzGQ6J$NjtwTF4l4s0(*lwSxw4^0?bJ#n@7vgn?^t{xwqUhEoFXXT4NH3 z>CpJI{GOt4Y}K0=oT(C3B0&n8i>-xJl(XUf9Da(KyjHX2UoKEEl$% zCfxn=c~5c9)8;IQc7UNsDS{yx-#HCnFmG!%rmwDQ%ng|fwPqW>Ya0DgUjrAFAj{mx zj?%`B2EJq^;c=g2y9Iv^M&BMQ7jc_Y_xq6{P~jRLNo}d#b9hQqdsqeRUTuuqvECQm z5UpA2rlQ{BGWI>VtV+0u7tN1Yv@0fgow;W6&4zUuEFaqCwEb7u+Uj_~v^;Z6gEBR$0wfsgo(@yk~^%c;XFdJ>rEsc9?UJjo=&y{pL z7!82t<-dtIx3b+DsJz&u!PhJukmavA(TOkRQ@O*fF-&ek@=+V89c4UTZ|M8F(fpKt zrIPIuahVy`dNMhhWbt2&6F3|__2FuoCeNledqh5pXjEYy&+B6(Xh~r?Go#{VcVO*t z(8!a*RUW;^cbM3PDXx72q4Y~3X|WK){1|85#^V<3MP30)L4h$6c-81sgPI8_n4K~* zuvnZn5OL+oaGyEz>-6Zh!|}J}tsD>JFa08wAzj{74eJXG5T+AEyOHiaYvV~F0YW@@ zbSn{S>*>wl{aJQPWp{n!0jt^B;{#xSk1TyV5lujdnR0PstvmT_v9V*%kuNQ(Z6c0C zcg$vZ0AO(wcxon{Q-(b4G2B;Om{ogm|9- zFrPys^gqu&uODzKE&krRCdk{iv`5T%W_34DlqpKIp$p=J0Q>!I#Ax^q4nTRm6Cce- zl>eu1CyW*R9evU8j@lK6NQNm6F@6!HBEcnS$`1!tqt9u6GfKm>(ePo?(%0!8iECom zCp+&qTfYRMogHk>T_jJN3I>9Z|C)`t*-}U{L|s~Y!c^~`9z@?_*2&Y!x^dJzGWB~< z&rs}d2bym~1BBsX>wu$?2K6Uj>0DTbGxEd|Nkf5Ye1EmHg*rI4CksbHPSf=^cB~{C z5*VxMyBDFEQsHSQ2%cZFnmgJ?(k^z8mpBYTc+B&iTe)CCDPc(-Jlx)D?mEw_d)LM%4J#?;eKL0TU~gU(&d-m^X2DM zT9M-BknG@dLIbn^4oey!yg+1)YX!okkPnM4m)-O;H?+^ZkugmQ1d$9|U5<4vYP9vl z5IVvY(;-WufNi?)gm6ONxVSIlc0v z5YWf+nU@4W(2k5^(3_Cs{i^3C^eT9sC?d)3Xpr}h{qe$O_S&|gMPLp5=>@exc=xBi ze-JSG`5}l|Rq2J&T~h#pf=WJ(PzPZ9Xu?#6q1VO&Tq#1F*&#dxyo;G=m~J0|XOJIr zfq9UGXwji?UvCf*){ZOMK@9*KY|!P2oH?z;OLe#ngX^*sS66yWg55BDc1R-JTvC>r zF>oVRYUWYh*Vs9#u*82J4|tG+bc0hB?n;Im(e=!};$syABAij9#&@m@ZJ!r2=4g}f z;e|D9z~ET5oXqbB0fVPZssP5jG@vSun`aPQ5k5X3nAUd_aVq z2$@xJgI2!;#{iw4b>6c1# zm`HJzYTfI>NP9WHeSF{K!Js}^am+Tb76$0fKEeFI*(W2N6afoZ`9mf>LC5H4(m%~@ zy6MH@xJsV0K0c;U)xd}zxuKs@wG8HFA30C6UL02(axxhy|6`_P*kCk<{wB40+7XI= z7?EhLp7pMuZa+T99Q;Jz1g~$;1QXO1##Zm-c(h#A&S)o)?7~JF3>o96_qTy%B6{I?Y1gM0kkML(u?$W$}rT4 zqdUPqoS^g?=4M!Q=JNVGO}0_A0AcDIZtVgy3+_BkIBWOCycp**PM@BJ=&*>@os^lI z6Xn`cR=wt?I-xf~^;Ju)0p55MPsQwe5)?$!<;A9T_?FGX)VUepVx{vQqYjogww>>) z?GdKKQ144&XKJdK%C_5M|K0zrm&kYL^QU9=?WWMlpqOb*4o;NeP5RZlTF;-mB=4Vrf|Bch! z>6tRCY~czV`8*?8gF6q`wNe8B(Z)hhPinr`WOXhjz~7<4Ge}y*o38d9k3LLavNds# z4DPck75;f|Kt>hozNAX$YO4QAb(LfL=~MPUg;#yek;$?D&dah3TVlI(G=S+P(jiT$ z^NUc8@kBT8*%hu`nT zHYzA9@{R59m=gLu*+O%@I2=wW29YbFl00?xlepeVZ;P!WFced}r#FP;ymS+81z%)y z1V9>)xIWVrrPO7aU=QnI>0Po97T09rt-v!P(#0IF2iqa!&`Vl3NW?Sx)CO)rDvXqj zWfp(ak~Ivsvn?rnK7UzQYH~7Y1>{NQW!pMXM!R2Z%68hn-RbCD(iPbN9Y$+3P8e#2n8kIparz3~Qoe!;BAjh^29M9~1uVnFF z3cUlM#Gra2aw>Y;;P1+1$3-SmlRSdz50kUkwL4|4b{SViF$e@ax12W+4Y_^4dcr$E zm;!P8#2F&ayIOXbAr7niu6c~>RgNb((&H)DvF5lEvK)pi6Z1*W;w;vs+s1<@QIm>b z0onYP)A0tC8A2R_t?R}fQ#0(!e`0T z++uq6xdYR?*qCQr06>Zy^ksAJuxZFOAtHr->GW9#}5;1 z*#+=g2NEKinM)0o4^j%mPq~8{8A>$;Eg*KA;!w4`S_ujDlam-l9^h^jdrl2&4vwv` z<{m?nj6cPT^&;&PhoTEftU)FLV5|lmKV6qxn;Qgo-xz8p62M&9jv_w8hJEoRlf_D9jT9M$Mb}L|RAkxeiautJn2@M6RFIWAWcDZ)*xBbDOYh$yd zP$$aD!716ZFR;P;Nv6-{N}``@D}~I4Z-_cm$=|jJg+N2RT}nr$TgE;E4bP3QrHJ}(6#1B41yR= zxTR7g+awb-ID21qAd4pBIh{AZR^(|9T&S3TGk_QVo)vJ@X(tW9$$L`nP@Ab@5C}vY zG>h__%tBNqc@JyrYowlScJFj3;ceTle!B;r14hTB@FA*^^BHkjcz34-RV|*o$TOPf zZP@TI&%BcdL?i-~tbzy!J$kj+krC)_;!J(?`#KA6n$3Pdg|(NG%{%6~`gePQi4>J3 zMu-WSlX(j#hA}RaiWOu8>^K|~P6I>uOS7}!4_*46{W2b!!xuk~9M|m)i1o#be*wwL4t^YweRQe3!*>@dVJ&?5vSALR?tzGn8qoy1lvvlG&U zAwvn8w4GBoncWyqxyLLSd(0njl-@TGknzgXPUd~>znJS!LlD{`BLpED!>_4h#oqVT z9NJVGsbA9ftf`Hb9dd5ToHcmC;We18+R+h zx7~G6(|X<`K6sPP5UQ_Hg}xO zaO-BoQ5e`!M~1B~9zC?v<0Q(q|Bj&Fxzk-y^cc*+F$4y><%>J}-1i%@(&D(VV>HTu zby5l(hs)~s_zXv2)RW!cmW9T2pBlXe!f6>+h5dJG?KycRp0zsAk!jDe?9UqKcMW?B zi_nTGxCuqu6mc%ypExeb*sXzgv*PH+`t!lPL(>Y2)G-x!>SqOuq|lq&g4ZImQy9Tx*3&m=gk zOvG8L_{5nEVrxDno8!$zle)IX>-FT)M~|&}@@k_{Db*oG7s@-|`etT*TLG9*vvcb1 z%}rgXm?<`ZC3AY!_Vj|$jo1D*tPcWLh+gOV=H}9aCwl>)(Q{=Azz`Jx6@q)V8YcL3 zd5z6i!LE29EO@t;hyZ_AsEfxo9h(R>*&Iu6B1TaiA3xAG6?HX|pxs|~yozO3CP^X` zcPr78gH`pNFkHCtsiMPt$)_3A{FB}`XyUn4)3RKg_@P)dQVk9%b!J|)Eoz#KtP%I6 zyP^m8p`r)19@%Z3w*3a$kv{oKUxe@d)Ns58@EZn(Y5?(gqW$7N5xTDpG$RI7qDvBG zt8c9R1nAdCEsCqJPV?mHMeTac59$rMF2V|s%YF|3Z#!FW3G!+{5*P#^>M+pG zuH96$G#xWWZw<~O3Bgy^E|o`{tpntJIjPr*sFP^sv%nT`DH-y@BsS?2*uHyhNJ5aV zvAl(t`e=lii^?|+2i_l%?JgZw1FjBlzg_&A?_o1HH-Fd~+uy5*y9N<3-h#l>rKiGE zT(adMk1qFe+SB~EUkL61ml+OzzW1Ho7HLOu_){DygI;8*Sqdb zTR^E*{m0$VNNKtNp!^1capb>Rjn^_7xedPFKSuNmK=yKruE7UiY@}c_yuus$Y>y(n`^@Yj6 ziDR5dJUmLQq=6@Qz0p+4PZb)t)#9}NHzCz{5^;5Lzu~Zy+K@;RAT)ya5Jj*ZKWRGs z(?N6s)``%Y_RS}TvdJNK0S)Cq?!R^2CY@MgiB2ch(xnG`dy;wSD~e*|C$2 z#NCqz@T9dK_JtDyGitn{sqIe4 zr+>`sNi*0owRV{_pc$+ey)Zuun%j6}^;rIa1Yn=`KH4AJzozc$divb0wZ)!SevJAb z=bNsjkvo$^)qV)p!97)%tzQ=67Ziw{_=l)M57~St%K=DL|9Z3P3Zbw`+$=TpP2$3U z;kNqyd9uiX_NJQLfR*x<<0bRJv}YE#W0f%!8jB#qz(xLMibHqwLT@NI%Jgb)H*)zy?M6I<1)$+;e{S0?Jre{`6b)2 z6#eV&jOGKNA|q{e9Zg}LDkI;5zquE#I|-9^;jCT!?*1KWW96G>1;n+hg;G_7s*^Ve zL?8n1R*hnAyWe=w09e9ygFb`BL|JMeZITFwJQb0d7@B%D@%r(srN5j2Kf?)i^|>39 z#HbItY2EsI9aFw3BQ3hdUMyw_m~Fb_fqAyy^8ZGp<+JS=XXLU#tvI|xNU=D5b0X=l zl@{P-S@HK`VCW_-`@EqLRfEhMr(}0y1mU6Gf=}c zgroRtT3Rz3Z=c=GG}b+6<3smKq~5#?C}_wIJcOug0Q$yMJ$wzEACDH8 zlVtqtO^7}*x^CM&bU{&@$jIq9qg1dA3;Wv3q1!@yOp|BOF?cKqIf7#g*RHispL3V~u+06_66<3xB8_@MeJtg9n8YsTeI zR?x@_$SuQ8C5AHHHm(h9c;T;NIds{D?4Uy5Tz#S>77peX3_ln{y$mwkG%%Yuj6;y# zNszdk;aDYu%~y|bsq$v(`e*941pBs*x>Br163CbbQYoBG0^;D)nZhW8m}9r4y1#~CJ4$fXeH#Fzt~B}t!a8MU&QF|EewKe=jFvm?135iA z7z9s3j@~#>_hQY4)+4+2@)LreEuwol?%`wmkewi%`@xIVQ1-B+HU8nETck42-Pcv8 z-VZ|9JN3RNtJRa=zV(R2pRCzSqj&W8FDCo2kgVyb!IPLo%wv;uW+uS7`A)Z`_1A>(#tAd8*lVQ0qW#51+lZJ+bl)lG#RZ=D(%MJja5XE{RP9+#{lc>euHGX2)|0=|(U#Ksm&H@KG1-(?b125B!TYCs zI7qoYh+3|R>Xgq_eblx*ImAMgZ$JF>kJ2=ZbRfu`Nu+fWJF(+KHs}ySSm7)-|98(4 zR-$Q{23|(AYY*2!_Dv6UCzS0S7}Ac(A{Ghvn8%P*K;1Sv#bOTM>QH8uzCT@BkH&kq zXn^sw$^!+5qo$>_0g=|PggfqKJ0=)EbLjbPFq@*!1ifG)R);ZoS1nNaemH5N?`xcI zV7n`jW-BKm)X7hRJaGQEie8^c%?R4xM~peoKrz0q9yI>-;l-#KP0ypcqmA!BZps;e zZpGVSqpsQ0l~4vbhxA(I2W>5|^Wf?0Wd~isu?0__fpd1o+x>qMt$>o&J4k=hAbl=D z7G4d)adU}uAu%mC4XTr%00M>ZI8so+;Z=W@dFUO$cH9wD8t(`Vg{?x-hQe>Nn5 z^9+bfU`Sf@1IOw_yB~w=n8@L7UHbP6C+X`x|JN-2g;; z+z~tX3&?8?h6XBjhQ(F!JOP-)+zLCvxKm**Y6|0N;;TSw!EeI7)*2R`H5&f}Grdnx zjlY=;GD*Lb%a|UW9Mx<{wFK|G0r=!3&{^bkH9yTrMAsfK!K>(8N>=h?2WluO7aqS= zJWC{c?2+P9wAn5KeDPvB1I=UKy3DW|Icv`luCYXGnEGwQt20Kbb z3x#px(t+hR(Kp_)rv;`1Pon8#{ws{cg6NVQdc?(mI;q||`Lf2&d*e%lNoDFYIIUJjTk`K_rh{Tb50}z59SwHUYTe}5_8sgand+cv7IbtM>07E?;zJ>3t z5@bG-I9IlrIiI0C*z`5u*I~m%IQwz=cZ)dD%Qea6nR!fJPqgSjsc>ELx!UDuNTrD} zw-I_(a(nL%tUhufmdK&#!LEs2hC1G~q`t;o;bLPihhU1uMF*VbEtqL9vKzzOWp|m5 zj|6m@)JEzgNht*VU@US_w?I(m%`pMRfre#;Tfrz}zwwd!;IoQe`_@XA3!hV*_b9Tq30M2VtD-Tt{*?a3qn>&K*b7@6oiRtJ z$~3X$n3ei%LGt=SK5!z6(O>rI8;dc9s>aYJr#%D)v^4F7FU?qtq#nq4v)nUzT3Xpg ztCMw9;6FT|;xiPB8il5Tx}@95!^3qt07zgG3sGM1fUS~~+*%O+vR`T^W-eR0&*c>} z@;4IDNa~F#9V_L}#bM-f-y|$f#`g#Pb=M~&F-ebe>akN?Nqbl2eUOfbP)J~oi^r!J zw%KKfAnEfPbKuwzL$0zX?)S`iy*QK@3KzUi$Vu#vCAzP;e!p%B=d599yF$ zQa@Pl4v`J~86Om5P)@^0H8Knke%A}}HQibk&fIUk*+k+;$$*yQkpMp%F|sl#SC#4& z>SED@y_xm&U5&L4$pCs}0r^NJBGUIpGQ}L)(g(9plsaPEmQ}C2o7MUWut1<4u0QXc zs%F=@O0a+CHUnvo1<&r*1?93oqL$667;4etHopLE1%w;J%rz{e&rSDHLqGx%gq(UZ z|7R+0*+nOw=$u*!B<_NOr|`p&YAu6-mZ)~}-}UtcYI!EKPI{R)GnU!P{%uDlrK-JH z$sKaiUmDi*mPZ%~@d~+*nJWEOo;KO)5epp3cHLDnWr#R$4bW3-z%WTyPDy7QO_Djr zlddc)YPaC5CKqy>b-pXfB$vQEU+72$65vlDyRN?b-CKjj{fTidP7-k{g9UC?7@$8@ z4Te-Z0#Tf!Yl$WxF8N?_E~_EHCXebEot+UV~*x} zrXXY-JL{vcm2nf8)w?m%3YuwqvbcG>>RmO)HNqsq&$m8wW3Xz#z}KnEH&~2&!Z^hq zH<&DRV(;`=ZlUJ>9F}H02vvT0Ii@y~si7)QX+0b+TW-IZlrC$>jWniEmUb530K2_E z;Eh+v!#&m1`I8eTwQ=Ei!LPP!7U|qsEn{$==)=&S|j7jSo`&T3>J6FbK z|G9QzbP8}v?sPHlNDu1E?a9T*Bilt50t=>pYIL`uYPL9giw~) zEBOBuE3jqo>b~PF?xWReZrt|}LX9h^JWHGDqE*NvlT;hzK`aL_QXDo({3j#3 znq_b^FDVMRZCvzccEk4tc04pRUKQx3ULHrg$tiizJ(ohH#t#m06?uE2j${0&yzAaM ziIT8c5$u9t_2$qfnanS)0C_WF@#Z_T75Mbgy6=-wklBevA~c!mjG@NNwKNmXI;muQ zpOT}OgUSZnp23Y1rz?00fm4{su751{7yB6pkDJfA3E*aAf`)B-RgC5Yv1<-oMnY@7W34(2z$bmQX_4Sml##nTxkx$d7h z?>Y;G3-}nRX%fzmsC^DwAyOm;ClE-?UM821Z=$it)>rE4#9WCYu%wn7!9Ps4U{GWZ zIo5fCsnc=N&AHX>_EAv1tar!#{yco~Tv5>PfMBF^AyO=O(W-djH?}N3W1pt0jh(I= zRpPY;t9I-GOz}N=7K;9)qAa@;zc<(u?+0X?h8Y4^^i}9!>YN&|KM`7?guIyN6YcPsgwYQt7e#Cj8eEJS^pIM%ZftGp?f zlVJRNv;j0*`!QII>ZX8pMxE*65WKsMT^bbv$pab&CcSf<^U(E?j~~e1b=rJ}7#M0y z5R=XjdEO$+l=)4J%-!hJ2~c83Ne)+&CG$^?_*bed08wRx;hoZ&J5jrWmSxo$JaW?@ zVh~zWO<`@t^!pEQ^F@Zgg05SH0IQI&d`7o}?4@M+4cS@3^+*P4_|zK*N1abLHWi~X z|Hf^$n6Qmr|E{ew^M0Kwdz+O1!cVn|U&_l&d&@pW{|U&I5uy0`n&M3a=yDxFE50#e z3<@XrWlyO3tM1e>3IwI&pW&IC6{Kl1Jk5KJP}4*%lx8=(DERGbhq6#hAefk4=f3%V`iS2Ml`??~SV6&5;wWe31U-cMiOCKM z2Wt`ouqDzFuvD691PV)gSOnQmj{k(TO9zB%cV+8)FLE{ULD}TWF;upQE-T_DJt5@$ z=#-wH1fU)L;xQ+BU-oeXI=m2TM7h^BmZVYm_^Kx#gD1bTNE>!3J2_1es`igfC@UBHKYf z7qPrM@zUKwFLMvCcvEA`x!453t@+!Hs)g{aD>~xU%+0$K-OK_IlJ;D?re~-5W%xme zzV=;#_u?A!iq{dmqmK-82PYx|t))HT`MslV!>M=hpe=Igmb8en>lW8RnYeD5lozVP ziUjG-!#rVA=%EdG=XC&^bq=!dCU*RQ%iVuAAdR57&4M7lY1C-EgO1#`nLV)Bt79+3hAs{ry>`A)8^ihd`F?hV8Y-* zbkAtr=!$$H{CMUocZIM?1feLSMxK0zQHemZy+}cV*MiJCK$N_Yl)n|wWf@UM=44$D zqrXrToTi`x?YMyqd0_u`QqfyFno9uRV{QhjyP}GJdFUuSwls}_&*LavT7HS$3;Xdl z!re;<=|$!nNKbZPixdlf0mHF9=XqEn+T1Q-`}^#}2#4!&l4v zO9+`wMDCDpHRvO^LO$H;LeArw+yOSgq>Qr-!b3f9==lkpHmLfdQ)~;{+;r?c3ILnG zHY_BR=d5&wGw{QMi`{)-kSc4-W(K;*=6cL+LBpuVwByG|7peb`Rw!!@%GYRyk=`?- zeYQp&m#r@y>%_}|0+Mfr7dN!l=gw40PK00FmJWMhjn(e8D@p;-)Z-j7)ORAc+Gx_s zSDL(0a3>JyVF%gG`@%9sJ+UI$0P3v zrAT;-HVAb-fdxmXLD~hGajLCeHw=cZ@8_d`^=l25WZyNmX-T!DjljY(?QjcvE?W)7 z+=lg8>0j!34c_FJ@3CJdobh5t;flx6aIz66tv&Zc8I9?U^qNYmuhUW)_5eDFfH%(M#5|X5Qivpvr&KDV zV}psFgJ^$8Zx|gufVTux9f|WdMHDQZ-DGhITh>PxW{QAetN*1#U@BNlE!XP-;YPcV z3oaJR(KWE4>(exmPPjp2t&lyxZ0a+0CE;}&*;PnIONs8LLl{2S0+aF_RN9JMjz79% z-sREbwgZFR4uHtD8i&aa{IO2CCDYr0*lW%?$!EWF1q_FppadTBh7Dh5M=A5>6G5}l ze0z!6ql(?_JBgHOHjzQs*K>UljBd&qmvp_~Y(s|^VmJT~sU0+{d`x!OWr(i%nLLcK zn~N&rT4OJt{Z{$2t9|5#O3}f8L2<)NzNCJjG55n9U^bhab6^cW)ywsa2Xo4N`f@qc z!x7HMXiBDx{SiYoQ!V5UpwYlbApyX^x7PrT1gR*~G6SOHy?8FjYW}X<9u8(o3y(?7 zp1Mw7f_ol&lZF?#I~ox^f08Tc1bv!q%jt8c!-bV?A!+y}`|YMmb2;<>BKdW#S&Es2qEG-AP6ClhYQp;uIzh=Io#v^}1Ca64f~^3TcQu zBmbo-tAu>9U(HMtKwNEGr@v5r0V$0?5;PXl_Gm5V2A;g1ZF)(j*hF zKjTN6I0|eK(Wcrcd`k9s8}F%-h}8VX2qXEx5XJ%lAfymkjL>i zfp&uDETuvA=_86WFtyV^VY(tADl;+C*mx|lAt?uPTmar@d?J$4I0JX`h6 zXPm2&;x%f>-|KAZu@EbrH2pI&ZAz&*PY^izT82x3Hxxm)&9=0#3Z*h{s@WgAVI87wm{(8-&$OVp*0rtF6x-X# z%+p4glkGaKpjkpk+ZRTpwNDoRtM~jJ4M#J!i&m(yvKf6zN8JxBvDXFN%QR~?c3WDe zn`P)8Q1#1ycsM@-0wAVq*nMfTH)8X70C`1k{aF7O1hHLAnVHgZj3qaE= zCbEvacEW+-Koz&I-6-d|c$1nxY9_%o)8K(!=#CN-GidB{k-pkP=-t%MHk*U3XcY40&Lp-OG(uCu zP9!#b5KkkO-k41RM1t(+hT@KU&uo1?KAZ+tbc}^1diZ=b>)%>S?)tu!W>K-C-evW1 zM={gb47eTZcB6u4bunl<70bPvmh{I3b@&$Jmg|L6jOX&Ao~M!H(uZ3^=2zmckcNdjwC=C5L4X2hUHW7zpZ28eK1VLLEP^UND|GNvn4v#9F@*6g!-0SQk?X(S zl@BAS*VhhnnN9qAU1!5?ZiL7)1 z?R)5!Lc*n4vwQa9%y5=#tA=2~AE#DY8x;%RC#Dpy9-)=0rB-~tRD#`B;SVCI+^^vt zLma_#Ims}Y!AUhni=~1Y9BjUyrx!(CybDgV$9%S=df}WoV3%)RiR99J#sH*7(rgz0 zaU6|IWgD&SR}Kq8iRLVPidXUPXuDSTEvmUplcA;OyaCNL^yle@u6!Pu3-^$8FEWN0fb1z{~a z$c<>C{E~-pF1(>A{6ttx=s^?D=5YW+38?mg6+iM2A0VqUpXPVyN-24%vIQe6Im{IZ zcC93}n&4zE{EBD%bY9rZ}Q>M0~)!en7mmDI41ss^q@I$Uw3KD&5`wrvI zWd3m|G?+oYnxgK@Lr%P2fR*z#`=t zt|2zbg9|APSg&kJR8oM~D3)4lUbE&uCnn=R_FQoACMswpo5=QRKKd~ex;_B+DpB%M zqLZO39K&!^(Q7JSNX68rzDj^aL82Js*9~j}s?zlTvE~&`jh@uhOBIU4Bz~ZRoU2D} ztS*Xue8yxQO9{LTAT83rRSF)bP)jCZ&hmFcy9SwEE@;)|vU=@f-1qFM;OHIuG7D6u z=wpD^4A|)rAV&zfEW;(GxV++uVEq(8qMMV4d47ld<9Ajcw%Mn|j)xGxa=um8g zfGZ29R0zd3@BV=|=JvG*H3c7Er?pnt^t_Ek7RL3yT1K~h}0fNj- zP~T&GmvwY*e3JTJG2alb2xuszQ!F;4Z)(?xdY4$kI7O=s;Xdm?L zhjk=@s8DmS-(DI?2Ju79&q+QBWMtshU7?{hKN-6P|F|vGWuvwY`c|u0Vu@o%8L#uZ z^puqVtD`45mauehuF38!JCSiCtv;v+y}J;Lhhu;v;5Te&P-w+WZEJ6S{{q#N+`ozI z(SFBO2;FMj`HL`E2)l(!8i}GVeLe2bT<%KbLq9>e>9&z5mEX!fut_QjZqe=~Xiza) z2o_4E6^Y8L_4v_#Tu(6Ss!CnKT1P~2vB%Y8dWZ|uLs7iGVeR6k_tHunGs7cMcU=-1 z4wT(ox3S1(4v)a6CXE0qLZ%WoJ7qVwkE*o#u`H%DFy-sWZD75V9bdfev!Fkr=#a^Wskm$%+i|Y#$RrKDpr_a=@5p zn%xWJeEoh5ZYvGme~9xyt{ILo7eKi!|64;|oCzTX0X|b{WoNGVwS9IpCbpRz^}qJy zy0sqkZ~a`7G;1h7Upy%iMg6PtJeozs7{T`L@Y<9|eB(x?c{m0&&EF=W}K$Y@#qg5il$2)!OOz~my%n|3c;DyEryq<^l0f$nt+d2J`kTPo<>HiYVE zFb1Tq6v9L|v*Jh$PBm3V72FM*imbz!9=N|-&#xOjn%A#9@rWqnXubRT?XmUC^u$~i z`CIAlB5*E&u8;FIqr>%{hx z2x-X}fNpkaBkYDqS4=#d%K6aak{y z-AdP!74cj6C{AtD`GH2X_dHnvyo~mbD&KL^V_3#h4JNjA9mF-^ypw?=`}a_#oP`Qe zTA|OK%Z~6`lDfnU*0WqyyA3qR2DESX;|8;1S5YafisqMNloxxyn+1M`ljU@JeC4L~ zP6e0z>JaEm+KKxVkW`rvLVVj{=AK>WI@Ky8dLhe@UK&^#f^JDTmeKYS9U$AoX#Uir zfI`jR2zZTp-F^GCXlv+U)p$DXPzXtRTi7}(y!?36P~>6rF;3%Oxr;v&Ge3duDp6y znmjzOxQONoVNtkDw!-#jxKbk}<>&6WL4CV~AySlLl ztyG4=xRa`#v)>4iHlW~t&BUiwT5UK}Vs=0rA#yIvl0C!jEztM5>u##|q~|@>u{x|X zZG8U?i>=ioL=&fD?0b4#Y`M7Ju98hpM$;j##3}FaX@ehjJ7|+&w?Ojd4NJ3NFbL#-Jn&r>}1=EVR!dT49(2j7O_nnPj=OA=RrItE72=XC*- z!Lm0|NrPN~PL2%cVMAQyHs5gWckQ>(-@wz*QtZatw@kdgxj+2 zg=WaWuH-Mm7G@jA*Z}9lxm9|YpxE_IyU54YBeu7lp~qz?`7ETaqJ44Q60?ITZ~R z4&FHDh3if*jC3C-|NCvpyledmSx@1=3Xc&pJ}c?B#{(6h%oI5Q-DTo0ggWr7U7WL% zwy!lr`?)mL6dW5iR|(oKk7AFF8*1a8?ZTPV}+ z;;U(uqu$E}(tS(T5$V^h!kLqa2}eF}^QG2pYCy zhH)tCZuGJT>o#=nEe5l>y=ImwLP4Q6^~27NF~UkgPT-%-j;fTm0S+A3kJ!IMkut28 zuekGOo|+2v`v(*W56(Tr1!V~ze$H5F(Kx=V3RDM7Dw1?Y>R5{56oBzvu0<)1lKeji zl4rc*13%f~EKxj;waG|OZU=kz2!b5`FP~^dS~D8j%CiQ{OBmGu?>#7;58N#o9RM%( zJB*j>d4|1RS(RhM{cH&oKs{6aUFu3Q2}>$bVzX2jOxTyadX-_TxG!{T91^si$AiyY zFb6q`q7#mPW9+m_EuDU+qMz zaMOYpx(>3-nLwBjSver0?z6WVnXel<-EZowSG1ps`n{N(H$^y_JRWx&QXRuLsQN;M z4ROb{Li*qiF|S;H!C*k1;6Q%^z69u@_A&5(%d?N_Rvyq4kjY`tZQ{OP4SSSLSDkaF zp+|~=w%6_jx6zTXrVkxqZxyhZOMt*4cJ!E8;ajQraae#w5XD2&EYIinj-%qr`+UPy z&&QyoM~iQk2u}%X1fQenzl!+pII)ZWVxRYY)D^Kr`glKtXDaqVF>RTQG3t>Y+(w|vi94@OW%*A8xt0sKf4#Wh(76CWLBZDrRl=j zq#>n_w~yG!HqT;qfW)tC7Tz+ZNcpv9H;`8vdQdg$1Ox9twM*BMjr5d9J=a3*#awfd zShO%$ZM9^B!8eIvPM(r_4_2V(XO~8C5*KJtWY!$8MvNTaFxl^Ow>2Weq^*T!XSC$v zXxynaG^O`)3spXf5xyuEQCgU7|BB0!3gHn9*y!h%d5p`V*tKY@F*CLzX!q7ZO8zYBABi1$Ov_ld4ymE1WOwC^8?a)r} zr>B;S981c=K{;Gh%Em#Y==7Z)u4PQy>e3bzg#N)@a4#=(W!p#|z?;IRxCz>MinM^1 zwM;mXmb^zH3>NY@@$u3@$N1o=Fs^Xz&I;z$AvkI-QRUcue#?f*9{g6PFPPGwAhcD@@@UhpKcv7;{XC8l8-X=&kKGEkDW zql+53uQ)@~pHThKhJ0=So=muX0d;B=JPZc=?Ts&!8Alh;WOL2>S0QNoMCT*p4Vqxg ze{_*J$yMj7a{@Ked`iarREOi#C2Mnzuuu}(vKs{IK1DXX!-!;(V8u3;>wXq}zz+7` z3saWH!dUC9Y#og%q_VgUG1WT&D-~(~Jxv{eR+V;AfJ1w^)GDhg1a*yRB@b}+jdfaP ze{*|7vw72EQ24DPNko*IJ8mctqAc`&4rPuJCp@9N36bT-M`Fw6tI$tx((k>}lQzkS zf@tzT(Bj*95c1zz=(;R?d2K>iygES_+btII#7a?9llnEtz}ewDy~5{2yFhe9qD*UX zArKLKB;$2Ei;qdX-`*K8^FSWPoM1m!o@&S4&Xxm*=F`A^^NrkvH@O$iVlq}@1NRC) z858OkYNH*0R4^;`Hk(xTYJ>Id-XAo1V%8$lS{JZ|sJefb;-32We7%aj_WMk?^Td*roIWj{9Ws-@6m9*IiW+dcy2n&ZwxxIb2hFlcT22TcDNXZ zTm`WQ0BTXtd7|Dy2VSN<=}_URrRAOx);uz5HkZ*-+vUcJk#2Ba`SSgYDMQnIRDixc z+rpAHjh1tFfe!ur!o!thRIQ4ps)yH+*ffFDsHoAK;gu4CNyGwCp|JGDa^xAjt?{!J zhdHS*L%(%ir!tevx-28@H8j*mhTl}o(W*Xr%uZkIT=>CzX?B`QFXXZzs-e3Pdb)Fm zTq=lz#n%C%?d0F!k+FBzGk5&9s+->c6q3%Mkxc~R9AA#-L>o9r)j7^!men{Ms@;y# z6-b+-0+Fs*@8osBQ6=e+gX8Y|i&9}&n%fQ%$IOp`eG!8DEBEbdde%hb%gH_=oC;0f z1Wwhj)MXR;q3b=i#(ps;*vsTMImYaI*0Z5UFCx8hckVQ^A7f5jjNPu^&cIKxwfClE zMjf3J3*{?2mJmi^D%pFn_n{WV#E)Ph5mqs$H!{Z*%)6EzeDOQNs?q#GVe7F4A@9m} z0sq9MhtHEoFIU27Am}_m9ddc*NXwnC+HL#_%S`i;UDKX=%AdGQpY!NENa1-t=ww#Y zk{^@-shi|6$p_I$#=7l43opg+8g=_zcTU!W^E&1xq zJ`1x#o|!odT$6`^Ox!@701ZI$zqTGrs21Yvy@8sDTZb)$9zqc$=Jp=l(YvMn5;SxQ(W-eo#n zJ2wU#{=D4R!>$`CR2JgUwjA^RX$Uj$qfr#kop97M=n*%c)q&fmu;;#6_F?gy5u) zV7t$=vfyl&3q9*@fdMNn*4v?g4#rN9iiYUrXQ`MIOa*_NV@>qwpA2QIq0NKEpErcA zhyI3{`^~$i|GD)1&e+f%TP)bY(z59 z(B`C$Qe$d)^KXj}tR&K;P(wL!Y%dpt+>;wX5rBh4Ag6S#aJtpHr*)^&%pj<=rj%J# z3cvTPPGoQ+$s%V^O=whu0Ujju%g+~yh>43-sjnBlqa7}o$mHElk@+FG__q;~Z_ zj3AP9^~DPsk%)0>C*}s^0@@^YIzJnh?7=KC^tr@;eGc4Ae-W&mz8i5o9pE=I9yG8N zC}HYF6IGiZ*fjD{mh}??c;o5>{8aIx(~_AT@xM;z2ecZ;v_F1=NCCDBY+3iof<+At z2*TggIllE|8U7ms53`WVf*i$%D)kS=BV(6{mlN=C2ZP&~b%~aaHt*641qhzKuK-pr z()*TPi)oC2hd3vHNjXwTkYKC$gHzRk7){s!dPFIX&PSf-J1F>8Xfj5nP@Y(?>Bp6* z34->iX+LRb!B>tHS2`YKO`Ld153^6$^aO9By?DX8;{|m0C7#0^6G<>`28qXcY4|bb zrnMm+;Y~K5%2o6DwoXY3aEz}RU?o;i++|jj<1%;$CqTJ|Bk9}rA)lbG@lABW5gdKR z2EV$^l#Ysmz*6Lx(DoX#N=Ll-&&d6~D~Vmo*@4=qCJ|j@&A6g*G5rP)(0|NkM0;#%m?d_f@(e#p)EHdz~==a#k;5{gOBzy3p~i21AFx)0~J zB${RqaDc-AOr>>0F{?BryQ8KSWlELJ4@S;bj%x&&z>|8-8Qw! zu$ncRrM95?Vyq18nBS%Ph-O+ppT86bD_);lK#DliY+6kOX*&!8;`G zQChSrcPR#^8ENABN(*IhC|r?LC%@TZ!2S!z#4oG8P^O*SMWcB1hlZQrIi^_d*&y*> zV-uI|xp8#b0YiXus9*MCngm9P4Osk8o=e`@p=7bdwaYdQAedUKByzqKKBn6FUpyQK z`a^v$MEha$SOM2p19<-Q-N6&cCy=?HbuhN_+xFxOoU$|58>>1$`V{q-dt(pWh}^gk zoV;h;*z*g|Z+5yXV?Y1w7Z%jm;u4Pj+{7Ua<6Ml|Ny%O1Ym1DifFl>50$~HSkFcA= zbZD(J)v}%{aBcfXYYvp|fNS4G3rN-R^9W)g_{H0?K6lzrfK|sF0pWxw-&Es3j z*d&b&>e8zV#GJw}Jx_nHs47>-epZ+JKu`R&ORJn88fLe-G0%-WW;oI7U^UGmBe^Co)J&~g?WF3Bcx z0%B9q_r0XZESkRYJProbftZYPDb$(45xDk?ZNT=?{$Et95-Hv0hf=+BH$>zx;$2FS z7mOtz+IaVY2UrOAvZ%9vn@Tm-hQY?es2S)H5IW>nR8>*MRhnR8;B9GjxavBW>v7jyV}y!NzFw4vI$SF@ z9Hg3&KcfCra1vBp370|+^L~s>#1#IYLpP?C(9?=7?NxN?xPSqad|G2vKfi4>=h1S; zD^abSw1_rcgl~h=l#a?LsQpEq1c!uVzAv{@r7$nmE`5E!QX2ovv*O*&|IJ_{SJ%KE zSXX+_MZuT~n%MfyTOeGzpPGYX{^6@_NDrM7BFaroU1psc9qg(xKGng&B{L%Nw2cu; zhvGwxP>J5?TtU7ie!aTrCGpnXQ^i~rJvwAZR)%pfhSFrtr{V`_vin_PJPGDPIJnif zS%EVNrU|6peQTx|iW#W>#&UikgND=cBmdCea;0r|2a_%}`blcLZm>b{i?K%lqJXr# zAIU6=4NGV(0;WQB&A%7cJV3sL#Gl)h9g95s#NXa4eIOcqzp6E#4 z7INRE`aYgB6Mc+{FnMlXp!?`po#Y9GFJtqFVNWDgC<@sapN@VRX{S&0Pp$S)3aLGS z{X`rzXRD?!qubcIpaMiQgC&A^;x){l8ySr>Qmo+AJ?8qj)U*h?lqgynJ@ID8kKYX2 zf{SuI|DU`=F+-qHRIu4|OIpY|8yyZCFv#F87+GHQZ7?)mtj5uoT>XL$wjMUQ>xzkh zpTAbf>{e`{uRAjI7?Ic9?7c6ssg1@ZRw;s%bGOp1VjTmhP}Ow5=}Pl88qsT)lV6e7 zQ9}{g7iyVAiHhVvphYGrk(X8Ynr7#nM%&e@<;Vu0QiUkj{Y-$_F;6&7#et)I=#<+0 z4OvAP|wHeaN0aUX5{WS=G7s5 z%6FSTxaD16PC8_p5>I|8;z|IFnL42SNMD)OR}r3C`Mb9wFqJ9KJQ+T*Ul@ul%a~#n zb<7zDTo8UQVqSB|dZIZsGei}Af{?4VDy-Eo0 ziT)0H24(27Fh6Eg4fWZX9&Y-lGYvuk4N7ca^mt-=CL0h#HQLBu{{vP?v;@|2sh7Vh zM-cA}Tfs$WO0^It5wU|%!);jJN<&fSP-YE%A{Tm}WDmg)z;zly-)ks+4qrJlMo(<= z#ZkuAd`0&S7t^E^%doM0B})Am2?@*JIBwaHN*TCU7J6D%lV$_2f@-p$fPE3 zJsN7b=BXQpzM>rR26#97bs0*Je9VM@7d7R7iSgj7!8K{OT&es0RAf9fUpWvygv+M2 zD58RzEs8K1@rKSH{Hw8znO2Z$wKQWbkgs_(fYH?1CYB1ZLhkvgIC#gu;U&kFS1f*V zNJ=~BON4LH5&C523z5N@Piwp1s zpiM*NEM3Necj1nAkuhQ(14hE9y(DBM?5pP49AGv?q4>Je$A-Ex+{rNjhd9r_ zP2#kU7BCl7?Bs!NwxH+zj});KB(};wh}81a5+!)6(>Gs7P~mK|5Zqoi_9^t> zzGFO=s2cS+OP4;~Zh#N5Dj)h3IycZIOXVL(^4YT|2vNE<+++={lWU`!w*2W7TohI! z;zQ<0iv%Tmg0&t2AfrzIKD_d zU>+p9^Wp?f9fQtsg(YdB z*iKRn5{4%!ZSXqGo(mT9XG#&cZw|@gwbXu@>mr()17}i06|l%M0f zIX+nV5H&VkU>}c1_dnZD0&xniNYsj$+h3l>ddZyV`7X(O$N3myo=etNzu`qqeu2Hg z2?;_R7H*>8mIQUq==)?mgiNaTP)z~Dt<>S$c#N5Uwex29*y6*SBQ_78{Rn$2!?uF6 zGUWZJ-Y*%`Fu{#iW**K^VKCgEl?bxEM!3>^!!I^2>q{|#dzuPKPY(*3B}Aq&Q*!c4 z>l|tLxu|*PL+AjqubnTl*K*%@XK>cYGf=x%*_VpTBJ;d_n?;AFo9~7|{e@q>zfq{f zMJ2!oRaO85+TW#Edy8fz<@K!?g2&4%pdD^nh7ZF4{htazozkyKPo^9DbIq zV((WnN?&5!da8+LXNV9Fo9rxA)of-}V@#6R7XJA+UYb`dWwejt53phV)+kM-7{EWcwxEWi8_Ke~@l-A|{c0{ct!C ziDiab9W9@1hC0sEs!mlK?~D+a%d=bLP%9%3`Xrv+u%qX^rK>$~-^<*i^fi>QPFF_O z=hdQolN|)?wT5qN=sBZjTU7vZ>>UB2{=&U5N3^}aPSH!n!ObF8wN089A|o!*)joC> zV#TrU;_Y_750%C|Io<{k@Vdptjg~l|ZMgrypfSj2qMPdwz>6u$sGzZJ;zHh%WU&h< zKvlh9HvT6uVulR5Ss+4{uLUxkfgGfQr-nZRhQZx`a*CXgeS3Vkw2kqOsA6o(&+EFW z^(InTc9fRzugsxQJuUozYdM0!(A?h4J8qAc(1?w6iX-CDJ(Y@<=6I26*BLjc`1+e@ z+Mm^%wdV_~2rP7B9mbsK{Y!-`O+D%WYti5eQPbd4mgrv`Y7_uBD{WHp0Tm^rrr9rH z+o34A+V2uRME9`n3)thYYgLyWYUy zK0-EU(n)DMxiJoSJ98H5f~7sS5}pJGwOZN;HS3d}3yT(hVW^l0Sj(B5zoJEayMS-9 z=MaxQ1NCK)Yhtz9H$NF|sL2byEKIJ& zgxnrrNi(U+q*bXoid`eQH6q!}!E?H74PjJ5tq}wLshj1%0ah$5a>V^{+!e$%>7?iD zoNVGd4*YHN3}dImidN$Q4eLkWvz+B+#r{u~%a(r3qqqaL=-`77O=u|wmywL*fh|-I zE3qF6iu{OTOrcHt=n~agATO-bcF@*0l4n|{XPFQJR`d8Kb`P;69jsVz84DnVVO4*L zPlUIaeYw3l-n+SKfheCv?pYm@dMNrRMX}^tfHY$z^iYXX0YlJMHtrv6+nT%|d5va_ z_zULvO#di#5UM;MlE+Tyx}=sp(|R}k@EKx|g>zgRX}5_5Or_Ln!^5~KP$ZI<&jOz1 z7d||8Q6J(Sf`go+p^qJKFTVIVzl<=e_~wUvW&VVLnSH^2<~F}0bMN~t1_k{5LsZ;Y@+XXS09k<- zN4p4N{>~?lm{M|M`^&T1U61-d#lXgbo*{PiUM{mCeadi)GVx7mioywHN3Tea4veM^ zzHR##I_4{TWE-T?+2g;ADBo`BA{csW;}~3&b|GsO6x$$xAKE{r=ETikGs??YAV%Xa0iK+GhJ3@hGbA+2*= zCsoa?s`xnrN;+Q21Sf_4GIybPB~86S-wG)%9m~Vy;9pU@2h$c!BoJVKq#PyNyCLr; zg3)O)#c@);2Ee@t}HR=Ci;W z1mm|^b{FjSoPYJ-xPQWFRxBCG6hzY7x=0$6Qt}D=LHVh2y*!Vu5<2`On*k_%9kYoC z^uBXAnaN+C}y$H(#irJd+rcrj&^e+O2nc}v%^Ym2@h{XUfZ z@zmICOuNR#{fjsc*{Hu%R>Jen1fdS;?K}3Zw;|ZljPa1Q0;!&)ynZ3?zs`X`d%}EI zLG~A1oO44PHnd&s7;vt6Cf_%(!Sj)^pB=|HYeaBv+`Dg#Sd}Jisajm%TSgxJRDjF?LJ4R#O zt+%uN9n?}KDoTbE+UVG{ilIn$It|O^VZQ4EDxD%K?8tW(3f>!8N5(An1n68|Y zs?QYts=)Z@T-bEJSOC)uqkyK0#R}-P4#bb?Wj{XNr9&} z!uiW*x_&xq$`1t5@c%gy)4_4#h1Y@JpmrUYxjM+a=}>-4SnCRW^p=Giv?m~D#~f|6 ztV1jNz$m=kDN5T^bvQY)1GJh`W%Q`GI9Xvl`a7CLOxz+6jx>tEbr^}ZaO4K)xX{JN z3W;0&=0E{_KQqD?`gx1jKt_pyuC+2AP&XpU{h+>l>d15&%hI>0lu=!Q>iJj`tO^BQ zFzXl?=39+TzLz zxz#x9#TffWYk@gB8JM2Z7=T6ndDw4*`J3-z zW1P}I7q;&EKFFa6Sle!qA%f|ac*9z( z6pnbA%ZcVnvzeo!D>)_6W8)*E!GMV~yhQoUb^?ii%rS-t8#7JbnjluvyE#0o`()6e zUD(qtf1P5@D#L1c-h4Z5zYZHO-Jd`@etQK|{FB(Um>p-zt5}eXs9LfXqg0kxYrm9m zpu`koE>pu{iYPXJX(S2WM_$+ghB3$uJyI#y+uDF2y0U|OgvFS>uu&R zQr<#)%kjuTfWtd~AL>VhOU|`X9vTnW!)jK@84K^3jQ~H}5@INeIeO46f>C$nwe~zM z*0k0a1n&ZwWfs-tIwM8XD^?1X6u0>iqkn#1EKj--WfViSOYmK`^9+N@?24%(|8NX< zUThjuGwoHz8cbCd^N@?k1du$g`W4j*u_0s|lj>7TWESX*4Mix{xvLgthd)7Gr1~hB zkVqD{_0PRvbt6beaux$K!7j3pV60#=%5OR@MsUSJu%5TS7I&7_H$ILZ8+G|n=_9s6 zc&>+2Rr_s%E@|9;^i#5pHCu{>z4?!_BSzvGi9ti>mPxd*d zRQvg(*vDvoEf%JpB46ED+#e2DVt|TxhuI%&F4|T@)wN&{#NJ1z$&8)j48Y{xsF3jt zijCd_9L_2CK2x&{TWtTpg#x)B6t+8`-LXAddghJsgB%d}6LgR7m==^5+@quP)aMj9 z^Ct{Lq6@VqZ0_O{1hrCki-DQ2H7;ii7;77>5($-0bb8{0o!+35*gLULEef;kXl}oSvZrffsA}!_@l+slKcy|t zi>3r!n)G)Y(9x41F&@NvTf8E@ci=igTl#u;@I`qX;_m~@&j}&0C53S zN7}`6FgjD(6J*G&MB30Ho}S~~U$=|R@$8DxIg%y2K{lQ+b@Z>JjV2zYvn!0qZ1W1= zCg4pul{YX<{Ky47z+}WfViS%GS)nGyiz)$bhEnju6c@NGmzAqTN6Q86eIU;7)apV6DVomR>zwnfoOIrYDb=O&&QhRgiCx&p!hsS%yFnU;p zZt%f}6Ade`pD$O0zZ^;`Y{xXJD%9V!jdHQ83Lu~!V=Av7HoPf&@q0r+CqxmDW(kX|WO8%! zVVB@W&3@!63K9cQvL8~uf+ocEF^iX?9tWzCJ69s7^!k4=%qA{q?O>Ws z^|AY@pZ#7kwq|?ekM6*i^wwpxz(e;Ckov&)1ddHL)KJPrzQ3I~1GY!G!@-UgJ$$Gc zo6DAgb&d)g(@&g((_|8~!Ps-T^FyKaHEg}mZy}^Cl}Lr-EcNl!j?insXrD6 z^oZ;6i;GF#X9)u>$|O$65V}TSPQKz2E3M>7oZb+c8Y`Tk=&yhCl%Wl3CGr>Q9vyAq zm`R4xvQSrqsew_h7V!W*St8yAno4`~fQag*R@;l9Y=N5$g7h?LjV7+4&?CQB6 zz9?*ys_fa=NSRQPS--S7X$60g(L`nykCe-KYd#79R4hAGg_z_orYlv&teaEO2936M z@|7KI6W?dau2P?gN0S&>8F5^K7dyhJhU>{~n?YFD{p@(^kfGVcO8FbFXCbw#b$8?kOc>4=q`h<}r-o!>KOt(Xb*7@JNSPSo2$ zP2kA9sex$~3m+ig$$W5j2vLALhDdXt)gT8Wo-4(w zmK6tk_8@+z{Iaa9Yv;3Uhe%F5klh{dz{d{=8(zi@MdMw9x0jC2Ow zHwZPZfT27AjZ^6Rm~%iG^c3nJJleNLk^8iH-a-Xj_cKdrXR0I;0$9 zYPCg-u_T+#{1`{j5~rdCKr^>2x`BvpnNEaPvZ+*w}8ZfQGVYJbXW=`@p4DrIJ?zD_$|VX3jFy|yMUikhn}~(XF~%a~JB&3GA}v6V(}Zv|K|$tx71$RxoSx_^o@~2D zD7QC}D})6)Xei9Hmg1AG`?!WDAV#jRKXi zN!mU;2;W`k&UB#0!k0+-Ls$i37dGIC%|}vY=1_tmdA6l->*SBD-~gF8MP!GhsQR+x z^4^!&)jPhaVpm2vOO!fF$bCfPufH}L7Krp(aMDwhXxrphZS&YV*h4S9tHt2|9-4rz z%p9WS|AHB60{0VDT#qu9?|_#UUeE1Vp)^mI^kMR#IrH{mXJyyM6Fd`qvyHC{%q)Oz25@xJb~g8F23m;o}if*&rno zL4r|r0TrX_|3+VKSH9L!@k>V&23cmR*nZWw&fVNz(fUgfd9?DA2pf8`VTn0Z@=% zjE2H#1;J^tL8XB_)tR6LYv1E+Z}{;QKk73caw@MGKQIXhS1i4w+b(NZy}`YF!COfA z$s5JdE_pfvvtd^~#*H8-4=JR!GZ;aps{BSbZ5+tB^2e;Ar%lLuhf}h=H(>nMh>bFE zw`>FiXL8`{6Pam~KskXTyI~asKDR$DFp4RiP+LYpbv#;W8o&d#mZgN z1@+@1=HX&AgX)*xVC!WkafJmR9wT!Dgb};++s)LG8I&B_XBzlHZH!m<2G&d^V)bxq0 z({%mfK5j`GJptto(XGiY=SJYrwO|p{CbD1TMDWI`v|3y%fJhQpL2e{9^}jf!$bSxs zWgEx>AC`*=Lhm;&duhM`J~-nWSCvV>X5utrc^$j72c~4RJNyf>N24=M<;IY7k8;8q z#4dY0Lh&phA{id6O1`7FypoxzF%nwKDZX&fo~gvyTsyPqA$MZEZRsFFYW0@>)f#vh z7g>F*OmlY(GeK?cW&BObI?5a|VNHpWbWu5W;KCtCM~ ze!_a)-Puneh*#|u#463`3aBYS6(c*9xVN2-#hTvAKEtNFkxIZaxajlXFrI$aDO7b~ z6npmJ=W@G1d~ZIL4}=y>$xn+ARG!%vkE&u+OENeq+MaW7wkh=yAbYTwN=MkMMF6~W zFZS57*5H1=`}g>vAy(+E{rgaa*YbR(an-DYNU+UnU$q)hYZ}}Dl#1e(Al;1SNi=5p zl~#DP-LV)&p{H3CJb&Msw<&_{tVDuLeTwT=sG*;!j)e-@bjjarM8v%orW4erJ$x5O z6=iZG(>x+cR|_o5zFv`g)brTqbH2C_hH@rl#1UHR(KfWN0Wgr@0%pC5#>2&`1YO5n zr=HhUWorb%VPpm0OJ5hQDdVE=vIG4fT$VPJlW7fJZ)sqUPU6uA*+g9lfe)+_y@;ZOmh3>~RV zB`Cgs9hshf_iHwiYAFsBl~JEq|MK6y_*co&?*R!8-AU2?_}&3D$b5g^vW$!pQ&v78 zMusd-gS>Yz(I_y?^WT}M2ClEU^OP1ztrKUD7D)xw5cj6=r&p5ej2|-`F2*=7B~HoM z#RZs1{nyut7pHhtPVR@&)}zauW+L_W#`!$j5e$gWj(iD+M~M%VxW~rwqN=2F70SdW z5;M9+s@D&qUUufW@iAHStFCFY+>88=R>jAAqtHf@VH>v+?hmm+5X{cC8GLwo<_20X z-0Py%P=G{M6D6hB&SA)X#ISQ$b6v|N&U_PC764@Aw0EwFre1wM z0TE8S+9F?U+bS9}@f5I^yN@4uwF?OjlICjv155#N0B&_8K|@oI(ah;%1YhX1;jnEdzwyZiEN2Pv zzaJyt?cP2wn{X+D8JES-C+p3u8+1^owZa!}Z_DGKkAnk&rCF)kU4&QaqI>4B3k+b z!sDFm|H{KFob!di)T0qO3{;;ojhE#hE&=&G_jA}6kaIzh4R4P#ij!6ax97*O77^||y!x{#r_^(Mhy#cUBX`hI;2S$MFY zxK0HZ}(`WR^x0rjl6G&%rNO^l2`@WFZ<>!S|`{U zqD&Z<{{|E2?8^FzKV&b=7(G;Bf`lig5wHykn7#4zGq7C@E=123N@uHlegF6}UTUaz zZlER*7&0}b`^i+23F_68!e$&-l~EW;rjq=sQY2(n$SC~vL8Jf}*Gx&wL3(g^V*tjt zbgFz1DkPI4w0R-{U9Tp%jQeT749OxUxduyZZ22czE65ha95vJ`6OY|!ubPRCoxX)!^K514LrITPU zrCb$|UWi@Ld>#)xDGEEp){R;6Ihg71!mMNm*=IY|ml3M7@sN|abM=6M`PoLJaFid? zIPr8A#VY|rT>7|0CM;qo&W^sxtO3G9`lY)9_EXdNTQoV;Hcu<3K?PY#{kAQ}Zj44v z=CwSi6ribUWKFH*%8(uQ)ckuvtgoUc*N?7CO80GGwsqo(P-EEUXb&q-#xF!6h~OUR ziQTtPNRwkm05?;utnH+f!^RSG!YrM9+`3Io#4R;Q8A-#me)j=4>}=6VK0A`77*9~E zEo~gIbV$@@qibFam1+{?8m~5mw$N`nqD5Kv7lX>5gC2n#dDsV^cZRZSVtu?W;XDm< zmt;!{rGU@P{#x~JY2E6cg=z0d`N%zrZX1>K1v5~Fc2dnsuhN&})xd!nMudV)Nxwc` zLx~vazPw@!%-O*&Gtyx;ztG*>bTicKh)$w&7sw&0Wqs1H`W zj@dj70XqC&9DT>C8&4>_UV$Sbp@E*wGGT>4!6a9kWI*5F`wPje`(*aysf>rc zlC>j$!q3Uz2#YInoyH3^l(X=t(twy4UR#S~Omvv>4!4yEzCtrHw34%QCBU+?3|*<-!mML;hlmRuU^g|DZ4{t<435?F51_#EDa;)<;cMVq7SCDo$rl&BAJ$*-7C>atyN$BL2$ zWOL_G!Dw~Dx|W&^TvWO%0@IKEBiW%ZS3&KA=vIJ|jo`PP=o%_f=nAwM z01pO8B$ka`3LBt#h}9c=^Q??_XiL^@(^kS#c0x0QV2t0w@+K`TWMAg} zr1=fpSR_o5k8-G@qEvu1Jxn4W*6}!r@u;0R2T@2_$}1FKzs)<;iw8e>X(Bv~IIWM1 zrZOt{KYLyNq(l)JiqNugmr-W*x&n05XJ>UiiOs*nFh-%PeY;i8tPJ3!cPLAL`*T(a zGHnRqz=X_1;)HV7Z}HmD(L0e|HfL7BnLoP4h$?Sk;xNU1At&?@Jv8#J2BFc$8g}5i zKag-L^~7oyqPif2&DIioPGQJF z)jhfs27Mb%|26PlCB5=M;cejv)V}_J8p4pJ$v9Zkv@p9e_M7JdL<8Gf{xD@^n)=y) z_ZiRA23qmF1-%}kRLe4`LN$4g&kc`U=Irxt#lU0?hWj_qM7~|5nbaF}0$@~8DKhJ> zOf(yHf#l-Q92pJS`IhG6Nfh21;9>GINM@HLR#GK%Ozc@H#?#! zPV1n8C`_gNH9}Y|?moCC@=x0zl<6fRqt>m-B}tgML}^zWU2pIb|6)SCoKLY)(GH@WMp3 z#D`;CeE)i(LDE<^KC@B~h|rr44Y8$6YW7o*@ z=r&9seGtn+ZQkudetMW#zq(j3$&itgO(7|0f zdklc4b)=QsF5mav=P;(|5%vaB4;6ctZ#6_g0kv@z9ZM1pgJa@s*}f7UvnTv5M<$)V z);}VSesHO(?Aou0eX4}+5$+d$estFStJ*T+?xYh&i8F*6iuq4O!Mo)kXM19DR6oj| zvsVDF3fT;$pgDJPaHa)0njz}Pz@qk-UwO0`hAJD4QTOnL46(qOhkIR9ryXyaV-1z0 zZchi>`YNm!Ew8?i3r|6O%gtX-9m6wpV{_e?3Rr=|c$C}x97Tv?Ypls8Zd%xZW(Roj z(DcMVcz+ANPI{d&L%JYt!{M)dG~BcZFCuSmd@z8$#6tv>Aaq_&{Y>mS&NsME5ss9| z6Y$04bjd(O9+U}il#}nauB!B~>Sy-ntMd>>`44Kib!y4rJaNnK@?>#Yc9)Bj(p5A>Eu~P{_CIXH?nc2AYpCsHeSokaq zq%8yx3HzO^)qui6OEL(wu4IK{Cr{z`jHGnQC1Sl3D{vO z)QQ$!x2(MT$2iu)K2^a@w6<^ce+U|U986i=HrIEfD45mxzb{Sc98_y>lhlOFgmt zYyFJBQ~pRxllmj8rD`X*XjUG0&)b7Zt7zb_t>krOa}`dTcPadO6`1842~Qu8NQB!C zCVOZ?K`V-wARcJoNTc%L<8cnzdBv1kSZcNwi(q+?5#mvlob$Bx7uXUr>edPJ#pI`_ z-1%h>AG0Aj^?h$OQ3fu$#$r>_Ewf;#!+Z}T2<^M%(LpDwzsfmbjqaQU$M0L&yjiEO zEeLm~-HS)>F=B{ZAbot%LThEpZ_I_vA$-TT?OW2A80PjSedCzT=z&JCwOVpQy8MO# zUM1t6!bq`q_3@65zo6b(U1{SRcOHkCw@9d6rhlblRDlg8-s)?IY4h)Ay&_-T5e-A+ zvofk!4DXY1MLPDu9@hVJfB!$BC;8C}&J<%=kvCom;S;Bh;&ZDR{ zqX6r24pn3Kz>4LfE+%HlLb56mIxF|0ZOl6>l+#$GLKi+%lyp{Xp293pm>_))?qV(8zGz?#G>1Ffp~ zip-p#E^)1DwM8(_yy;t1=A15+sC1YO<&*JYd~P?F7MSE#t)qX>Tn9FTLw7NUvVHDs z^YE}W=P{-NTRNwL4CXfm5J#uzC_NQy?xg^%0`p9x;z(4KB|zNG`5(i!c{w~l&XpaG zGvva(uWjlOhInh~rI=8LN|G+e~$gLd6CflAN$ zS4l6$#<#k~nqsoirfdHDSTgq7SZ_&*?5u?cL=h|6)apXrQt-8=I7twbp zi`YNnS{lnL8QSO4(}Kor2SpiS7;6m!@~J`*{8}lVAT}8wWTO(pCx7}Jm+5gP(h-h0 z0rigJ?lW=gbOGyvE6|TC9{VME)zPvJ zE1~*P_y7e?l&>IX5$jvPy=g6^OURP*pOx{>)iFe_H`lqEDRRJ1n~buK!^-p?xEx@U zGXSIypRkS09qHM?;<@m;Y*upX;hXxpw_r8T%^ABYDhcIy3OnU5E#$KKnhvOh_gc| z<6~~MYEwXL>9TRj2by!k5Kobq?t>V~7Q)d_IVZ0eQobk-FUss3BxNzel^j|@gQ3u0 z%tZ&O8wF^jU>DF@N3Ma7IzMRx6_~et`~0}j-^Nm}?l7}Xkc#?j6aa+va@k;k62#hh zm8LDG4IOrV^`K(FIhj_M(8HMHnj+F68!DwT{B7ikd$X@5IB(pCe6*G1ffVzuR7k>D zl5lg=`LNmzCntRs+o0@M<70X}GHS!~i1D~CK4)cz(^_3~BMkMPD2u+dBMXpA3-Ism z-F#Ri)~&%XQI)7ZH4=Cg<{o+L=ssviz5K9&WQGwPD#xHem-Tz#2wy{@rcQY6ojpij zANd4gw>AiN^Pm%w1lrG^L(dQVSz+4!Mxh5o>1-^5*`-4_7FbN%IKm949ppd;>WTV& zLrvK$GRFh|EAE4y!sMKZy3hj?#f>1rpIfPC9(~yjCvzl6cr2TE>{X87MRN)@l%=F*U(`| za|NlC5&hI)_e46s_&m0pj(NV(Ehd0w0*;BfbM&#?XnV;`%%DVukz%*YZ)u$pvA8By z^lLl<0%)~7He_Wg3&q$kLmY2xGN1okJaSO$f&|`v7?aOR$rB8wy~1x!F=@{;Mql?3 zGcbYh^xT%8W}3z}(0GBz!lX@&8shWYRJFHAK5e5matvi3<3g z`K9zdIYo!8&~C30Bar6ivEQkQHqt$$VYQ}(o?ebehN+7eQ&nI;6A+hSDIEjl2W&EyTgYtjp#nlILL}Zzb&RZG3OVyj*=Rm+|5w<;KMDv$d*gu&b4q6Qv$_ETfr-^Syg%R# zg;DSpH#3v#sFgIa2nr_`xlzPB4eU16&&l1yg}kp*BPvqRmkG*lvf{Ddq%pQ=FuI0s zJ2(^L@EAh`!M$>8huA7PyYl5S12b56gzjcypUXT-h34XcEYDf@f>kd29)b^i14Z-|8>uD#dztQ!>S|*g;NT- zSMRyYNZyD1z*8Cq^A+#W_dvA@onKSxT_F@>b3}G6-w%^EAN7N|XullxDQXp;Jk}zI zFXU8ZhXv%!cI+|2YlC}a7AU|jv;U*rhAqdqalQJZd%s0H5S4y+UE!@+IADSxvQDAvOqo;(UV+-N7s{H;QdvVxVZ#Eb0z|Njj(o1pH?+wiAL+zAn3tO9R&LM2n8t`Cg8~ zmO+HDen}ewF+ERMPRQVYd{igYkK=qI=J5lY83e5>yUBOQp_fvBAM1b^52a>(EHp9q zaD! zF-rF<^NSgR#?PS01tqr}D_@Aw2$u`lqaIONR13@H5R9m7Jn8@${6axTggHjx^FHBzZ62PwE zS>a>a1aycmLEU|hF zpD@=qPIatVT9W{cnur^UTV(eFc5k5RSM1~oky|_dlKPkdX3$Pm`^~?@RYduIG{n7J zQYu`fSN-!qmX6^?aDcay?T+29STnAAUF&lpOg@pZ*q?Cfny@+6qqJ{|e=*$KGcDX3 zTY6+WHi9<;h#-MMarJaCdc#?Sy3rU*NU%SKi&8zQhfhqyZ!jlyiPrE9zPV@^p=_WU)Z`fjX zZ2NlLMfrH77%N@`e^1D)ykXDJjc(Plh)ioU_bB?THD__Kq=aO$jf{b8p z2zegthQll7Aw=S0#%@T}GL%XrD;vNBS>3==3TZ9wAD8E#3Ny`!IYMC6;7xyU>iw)# zK{-i?-M_GWZJWEpinb`0s(71lkNe5o+Mldl<8%NmPn4K=_{^ zlzcww8Oq2hJ+1oUj34|>*Ff)^Xxc%NrbDg!YvCE5ZNRn&yMx_qeD`KcTneajj_Wly zR_3)bbgR?{T8VHYBl&k2yfR$tp9#Au~vbT=a48|Cp zW4KI`eN=%Kqq=6>eY)Mq{;#N(W3GczmeZOpol$G644E|gASY>9E*q<);^*pSRsNpJ z0+(3QxMqC*iD|Y;fY~8ErexdgR-YR;tY;Au0(*Nq%!piGPJrA{k@$KBq#^3%G~B0D zEmL3lz67-c$4}U__8#zDQWw{|1+dZ*p%|NhMZQ6;If7O!xi@_}NG%+F2hhx)?^DS@ zRFWo;K@98o03kfc-9;S#YP|Dflz56M!|NFQkq{4x3Eeo#j{EtoDNlxgCN(}kBK^U8 zghV6B1Po9E#W|X_g7_MpP}+Ftj_)K(AOe$c9jU}lebS0@gut`2Os({siF$g$a1saC zQM!03DD;eiDG6FYcIW%PbJP((uj<;%66i^7oao#w3~gf;-`I()0M?U{i0&k5wmAY! z99hnKl9ovK0Y}v@Pcpm4F39{fNDEr@Lv!qEhIGI>3-6#lrtUPL0o76vP7F}zIiFG~ zk)<~bfGS{M{WE=IN<``C8#3mQF(?-tIxj`V5`7Av9supJLB@BmOox(6{BMm;(IQd{ z5yOjgmeX)j4UQ@e2p8wprYu%$YYD~BG7UEGOrm8&=}zZfh8V^cnoxON0Z8px8?bFh zqV{>-tN9n0V=%RJe{^h2<33&I8TZHH1w9Dc+mN@+Pfm5miJ@DOr2Z=lCKWv%hWJ=! zsTw-3hf(Ux_ajlV1u_B039t-vKZ}V6?;-A$}zsX%5)}5TzbiuI_1M7s#*I=+v+2a*eZ>y>Duse6~Y27=AduAp0Khh znWyerY(+X=>xCHb6c_J)4d7`I1@hzVNdx=2koEaPk8xw6z4$;YVl9ip#C={#=H!Xh zXbpnDsd$qUVP;hr>Yr~n!MB&pTKC-cjVPJ&MJ5=S@b}?x*T8`fe#dq3S~R-J(q8VB#;LFR=m|SqDb(vXQ=aXFyKN3-jhTs((#B_+|WV9P+Z( zEfy>*G9Y0f2P<)CrFjn(lZ6Y1D&aq;&=-NN87CD6ANY`TL%xii75aeii%Kz_ww>2@ zd+E)bQl~|9hNDCLc|71pip;MyBG?4kgyG5!Uo-gzFV-hCC~p!eKR+!qL4|>LL0Sm{ z(xu^qL=hf+oBuCH3Vwn##D%Ig%0D%`^C=;2pm-Cz112N#MDP>`!(^&bC2b^3d|nm+ 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

b8|c3OVo6OVZqHA-L@iW28Bcmcbtcc| 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%0AG3 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⁡(j)H^`n&T9L)O7}bkJV>LB<%n)a6H4R73 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¾Smc)%G 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 literal 0 HcmV?d00001 diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/MockIntegrationTestObjects.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/MockIntegrationTestObjects.kt index 57a23fd391..ebf069b900 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/MockIntegrationTestObjects.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/MockIntegrationTestObjects.kt @@ -41,6 +41,7 @@ import java.util.* class MockIntegrationTestObjects( val d2: D2, val content: MockIntegrationTestDatabaseContent, + port: Int, ) { val databaseAdapter: DatabaseAdapter = d2.databaseAdapter() @@ -49,7 +50,7 @@ class MockIntegrationTestObjects( @JvmField internal val d2DIComponent: D2DIComponent = d2.d2DIComponent - val dhis2MockServer: Dhis2MockServer = Dhis2MockServer(0) + val dhis2MockServer: Dhis2MockServer = Dhis2MockServer(port) @Throws(IOException::class) fun tearDown() { diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt index 5e73d10fb5..ac179f0bb9 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt @@ -27,133 +27,152 @@ */ package org.hisp.dhis.android.core.arch.db.access.internal -import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import org.hisp.dhis.android.core.D2Factory import org.hisp.dhis.android.core.maintenance.D2Error -import org.hisp.dhis.android.core.mockwebserver.Dhis2MockServer -import org.hisp.dhis.android.core.systeminfo.SystemInfoTableInfo +import org.hisp.dhis.android.core.maintenance.D2ErrorCode +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTest +import org.hisp.dhis.android.core.utils.integration.mock.MockIntegrationTestDatabaseContent import org.hisp.dhis.android.core.utils.runner.D2JunitRunner -import org.junit.After import org.junit.AfterClass +import org.junit.Assert.fail +import org.junit.Before import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith @RunWith(D2JunitRunner::class) -class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould { +class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould : BaseMockIntegrationTest() { - companion object LocalAnalyticsAggregatedLargeDataMockIntegrationShould { + companion object { - val context = InstrumentationRegistry.getInstrumentation().context - val server = Dhis2MockServer(60809) - val importer = TestDatabaseImporter() + const val username = "android" + const val password = "Android123" + const val host = "localhost" + const val port = 60809 + const val serverUrl = "http://$host:$port" - const val expectedDatabaseName = "localhost-60809_android_unencrypted.db" - const val serverUrl = "http://localhost:60809/" + val importer = TestDatabaseImporter() @BeforeClass @JvmStatic fun setUpClass() { - server.setRequestDispatcher() + setUpClass(MockIntegrationTestDatabaseContent.DatabaseImportExport, port) + dhis2MockServer.setRequestDispatcher() } @AfterClass @JvmStatic fun tearDownClass() { - server.shutdown() + dhis2MockServer.shutdown() } } - @After - fun tearDown() { - context.deleteDatabase(expectedDatabaseName) - context.databaseList().forEach { dbName -> context.deleteDatabase(dbName) } + @Before + fun setUp() { + if (d2.userModule().blockingIsLogged()) { + d2.userModule().blockingLogOut() + } } @Test - fun import_database_when_not_logged() { - importer.copyDatabaseFromAssets() - - val d2 = D2Factory.forNewDatabase() - - d2.maintenanceModule().databaseImportExport().importDatabase(importer.databaseFile(context)) - - d2.userModule().blockingLogIn("android", "Android123", serverUrl) - - assertThat(d2.programModule().programs().blockingCount()).isEqualTo(2) - } - - @Test(expected = D2Error::class) fun import_fail_when_logged_in() { - importer.copyDatabaseFromAssets() - - val d2 = D2Factory.forNewDatabase() - - d2.userModule().blockingLogIn("other", "Pw1010", serverUrl) - - d2.maintenanceModule().databaseImportExport().importDatabase(importer.databaseFile(context)) + d2.userModule().blockingLogIn("other_user", "other_password", serverUrl) + + try { + d2.maintenanceModule().databaseImportExport().importDatabase(importer.validDatabaseFile(d2.context())) + fail("It should throw an error") + } catch (e: D2Error) { + assertThat(e.errorCode()).isEqualTo(D2ErrorCode.DATABASE_IMPORT_LOGOUT_FIRST) + } finally { + d2.userModule().accountManager().deleteCurrentAccount() + } } - @Test(expected = D2Error::class) - fun import_fail_when_database_exists() { - importer.copyDatabaseFromAssets(expectedDatabaseName) - - val d2 = D2Factory.forNewDatabase() + @Test + fun import_fail_when_account_exists() { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().blockingLogOut() - d2.maintenanceModule().databaseImportExport().importDatabase(importer.databaseFile(context, expectedDatabaseName)) + try { + d2.maintenanceModule().databaseImportExport().importDatabase(importer.validDatabaseFile(d2.context())) + fail("It should throw an error") + } catch (e: D2Error) { + assertThat(e.errorCode()).isEqualTo(D2ErrorCode.DATABASE_IMPORT_ALREADY_EXISTS) + } finally { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().accountManager().deleteCurrentAccount() + } } @Test - fun export_when_logged() { - val d2 = D2Factory.forNewDatabase() + fun import_fail_when_invalid_database_file() { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().blockingLogOut() - d2.userModule().blockingLogIn("android", "Pw1010", serverUrl) + try { + d2.maintenanceModule().databaseImportExport().importDatabase(importer.invalidDatabaseFile(d2.context())) + fail("It should throw an error") + } catch (e: D2Error) { + assertThat(e.errorCode()).isEqualTo(D2ErrorCode.DATABASE_IMPORT_INVALID_FILE) + } finally { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().accountManager().deleteCurrentAccount() + } + } - val exportedFile = d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() + @Test + fun import_fail_when_no_zip_file() { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().blockingLogOut() - assertThat(exportedFile.path).isEqualTo("/data/user/0/org.hisp.dhis.android.test/databases/export-database.db") + try { + d2.maintenanceModule().databaseImportExport().importDatabase(importer.noZipFile(d2.context())) + fail("It should throw an error") + } catch (e: D2Error) { + assertThat(e.errorCode()).isEqualTo(D2ErrorCode.DATABASE_IMPORT_FAILED) + } finally { + d2.userModule().blockingLogIn(username, password, serverUrl) + d2.userModule().accountManager().deleteCurrentAccount() + } } - @Test(expected = D2Error::class) + @Test fun export_fail_when_not_logged() { - val d2 = D2Factory.forNewDatabase() - d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() + try { + d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() + fail("It should throw an error") + } catch (e: D2Error) { + assertThat(e.errorCode()).isEqualTo(D2ErrorCode.DATABASE_EXPORT_LOGIN_FIRST) + } } @Test fun export_and_reimport() { - var d2 = D2Factory.forNewDatabase() - - d2.userModule().blockingLogIn("android", "Android123", serverUrl) - + d2.userModule().blockingLogIn(username, password, serverUrl) d2.metadataModule().blockingDownload() - assertThat(d2.programModule().programs().blockingCount()).isEqualTo(2) - - val systemInfoWithExpectedContextPath = d2.systemInfoModule().systemInfo().blockingGet() - ?.toBuilder()?.contextPath(serverUrl)?.build() - - d2.databaseAdapter().delete(SystemInfoTableInfo.TABLE_INFO.name()) - d2.databaseAdapter().insert( - SystemInfoTableInfo.TABLE_INFO.name(), null, - systemInfoWithExpectedContextPath?.toContentValues() - ) - + assertThat(d2.programModule().programs().blockingCount()).isEqualTo(3) val exportedFile = d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() - d2.userModule().blockingLogOut() + d2.userModule().accountManager().deleteCurrentAccount() - context.deleteDatabase(expectedDatabaseName) + val fileMetadata = d2.maintenanceModule().databaseImportExport().importDatabase(exportedFile) - // We won't need to create a new D2 when we support database deletion (multi-user) - d2 = D2Factory.forNewDatabase() + assertThat(fileMetadata.username).isEqualTo(username) + assertThat(fileMetadata.serverUrl).isEqualTo(serverUrl) + + try { + d2.userModule().blockingLogIn(username, "other-password", serverUrl) + fail("It should throw an error") + } catch (e: RuntimeException) { + assertThat((e.cause as D2Error).errorCode()).isEqualTo(D2ErrorCode.BAD_CREDENTIALS) + } - d2.maintenanceModule().databaseImportExport().importDatabase(exportedFile) + d2.userModule().blockingLogIn(username, password, serverUrl) - d2.userModule().blockingLogIn("android", "Android123", serverUrl) + assertThat(d2.programModule().programs().blockingCount()).isEqualTo(3) - assertThat(d2.programModule().programs().blockingCount()).isEqualTo(2) + d2.userModule().accountManager().deleteCurrentAccount() } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/TestDatabaseImporter.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/TestDatabaseImporter.kt index 38c596a871..4ea14052e8 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/TestDatabaseImporter.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/TestDatabaseImporter.kt @@ -28,44 +28,36 @@ package org.hisp.dhis.android.core.arch.db.access.internal import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import java.io.* +import java.io.File class TestDatabaseImporter { - fun copyDatabaseFromAssets(filename: String = FILESYSTEM_DB_NAME) { - val context = InstrumentationRegistry.getInstrumentation().context - val databasePath = context.applicationInfo?.dataDir + "/databases" - val outputFile = databaseFile(context, filename) - if (outputFile.exists()) { - return - } - val inputStream = context.assets.open("databases/$ASSETS_DB_NAME") - val outputStream = FileOutputStream("$databasePath/$filename") - writeExtractedFileToDisk(inputStream, outputStream) + fun validDatabaseFile(context: Context): File { + return copyAssetsFileToFilesDir(context, VALID_DATABASE_FILE) } - @Throws(IOException::class) - private fun writeExtractedFileToDisk(input: InputStream, output: OutputStream) { - val buffer = ByteArray(1024) - var length: Int - length = input.read(buffer) - while (length > 0) { - output.write(buffer, 0, length) - length = input.read(buffer) - } - output.flush() - output.close() - input.close() + fun invalidDatabaseFile(context: Context): File { + return copyAssetsFileToFilesDir(context, INVALID_DATABASE_FILE) } - fun databaseFile(context: Context, filename: String = FILESYSTEM_DB_NAME): File { - val databasePath = context.applicationInfo?.dataDir + "/databases" - return File("$databasePath/$filename") + fun noZipFile(context: Context): File { + return copyAssetsFileToFilesDir(context, NO_ZIP_FILE) } + private fun copyAssetsFileToFilesDir(context: Context, filename: String): File { + val outputFile = context.filesDir.resolve(filename).also { it.delete() } + + context.assets.open("databases/$filename").use { fis -> + outputFile.outputStream().use { fos -> + fis.copyTo(fos) + } + } + + return outputFile + } companion object { - const val ASSETS_DB_NAME = "test-database.db" - const val FILESYSTEM_DB_NAME = "test-database.db" + const val VALID_DATABASE_FILE = "export-database.zip" + const val INVALID_DATABASE_FILE = "corrupted-database.zip" + const val NO_ZIP_FILE = "test-database.db" } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt index 2ad13ea217..5d7958eb42 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt @@ -55,8 +55,8 @@ abstract class BaseMockIntegrationTest { lateinit var databaseAdapter: DatabaseAdapter @JvmStatic - fun setUpClass(content: MockIntegrationTestDatabaseContent): Boolean { - val tuple = MockIntegrationTestObjectsFactory.getObjects(content) + fun setUpClass(content: MockIntegrationTestDatabaseContent, port: Int? = null): Boolean { + val tuple = MockIntegrationTestObjectsFactory.getObjects(content, port ?: 0) tuple.objects.let { objs -> objects = objs d2 = objs.d2 diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java index c2c41e1823..e2f276dfea 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java @@ -37,5 +37,6 @@ public enum MockIntegrationTestDatabaseContent { MethodScopedEmptyEnqueable, LocalAnalyticsDefaultDispatcher, LocalAnalyticsLargeDispatcher, - LocalAnalyticsSuperLargeDispatcher + LocalAnalyticsSuperLargeDispatcher, + DatabaseImportExport, } \ No newline at end of file diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt index b75c879b35..31440f9a26 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt @@ -44,12 +44,12 @@ internal object MockIntegrationTestObjectsFactory { d2.userModule().accountManager().setMaxAccounts(MultiUserDatabaseManager.DefaultTestMaxAccounts) } - fun getObjects(content: MockIntegrationTestDatabaseContent): IntegrationTestObjectsWithIsNewInstance { + fun getObjects(content: MockIntegrationTestDatabaseContent, port: Int): IntegrationTestObjectsWithIsNewInstance { val instance = instances[content] return if (instance != null) { IntegrationTestObjectsWithIsNewInstance(instance, false) } else { - val newInstance = MockIntegrationTestObjects(d2, content) + val newInstance = MockIntegrationTestObjects(d2, content, port) instances[content] = newInstance IntegrationTestObjectsWithIsNewInstance(newInstance, true) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseExportMetadata.kt similarity index 92% rename from core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt rename to core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseExportMetadata.kt index 157fc223a3..1fe5ea5cd9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExportMetadata.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseExportMetadata.kt @@ -26,11 +26,12 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.android.core.arch.db.access.internal +package org.hisp.dhis.android.core.arch.db.access -internal data class DatabaseExportMetadata( - val version: String, +data class DatabaseExportMetadata( + val version: Int, val date: String, val serverUrl: String, val username: String, + val encrypted: Boolean, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseImportExport.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseImportExport.kt index 81b37bf5aa..34ee5233cf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseImportExport.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/DatabaseImportExport.kt @@ -30,6 +30,6 @@ package org.hisp.dhis.android.core.arch.db.access import java.io.File interface DatabaseImportExport { - fun importDatabase(file: File) + fun importDatabase(file: File): DatabaseExportMetadata fun exportLoggedUserDatabase(): File } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index 8e3e07acee..8d1d77e194 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -28,26 +28,18 @@ package org.hisp.dhis.android.core.arch.db.access.internal import android.content.Context -import okio.FileSystem -import okio.Path -import okio.Path.Companion.toPath -import okio.buffer import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory.objectMapper import org.hisp.dhis.android.core.arch.storage.internal.CredentialsSecureStore -import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper -import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationInsecureStore -import org.hisp.dhis.android.core.configuration.internal.DatabaseNameGenerator -import org.hisp.dhis.android.core.configuration.internal.DatabaseRenamer +import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManager -import org.hisp.dhis.android.core.configuration.internal.ServerUrlParser import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.maintenance.D2ErrorComponent -import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoStoreImpl import org.hisp.dhis.android.core.user.UserModule -import org.hisp.dhis.android.core.user.internal.UserStoreImpl +import org.hisp.dhis.android.core.util.CipherUtil import org.hisp.dhis.android.core.util.FileUtils import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton @@ -57,25 +49,24 @@ import java.util.Date @Singleton internal class DatabaseImportExportImpl( private val context: Context, - private val nameGenerator: DatabaseNameGenerator, private val multiUserDatabaseManager: MultiUserDatabaseManager, private val userModule: UserModule, private val credentialsStore: CredentialsSecureStore, - private val databaseConfigurationSecureStore: DatabaseConfigurationInsecureStore, - private val databaseRenamer: DatabaseRenamer, private val databaseAdapter: DatabaseAdapter, ) : DatabaseImportExport { companion object { - const val TmpDatabase = "tmp-database.db" const val ExportDatabase = "export-database.db" + const val ExportDatabaseProtected = "export-database-protected.db" const val ExportMetadata = "export-metadata.json" + const val ExportZip = "export-database.zip" } private val d2ErrorBuilder = D2Error.builder() .errorComponent(D2ErrorComponent.SDK) - override fun importDatabase(file: File) { + @Suppress("TooGenericExceptionCaught") + override fun importDatabase(file: File): DatabaseExportMetadata { if (userModule.blockingIsLogged()) { throw d2ErrorBuilder .errorDescription("Please log out to import database") @@ -83,52 +74,64 @@ internal class DatabaseImportExportImpl( .build() } - var databaseAdapter: DatabaseAdapter? = null - try { - context.deleteDatabase(TmpDatabase) - val tmpDatabase = context.getDatabasePath(TmpDatabase) - file.copyTo(tmpDatabase) + val importMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.delete() } + val importDatabaseFile = getWorkingDir().resolve(ExportDatabaseProtected).also { it.delete() } - val openHelper = UnencryptedDatabaseOpenHelper(context, TmpDatabase, BaseDatabaseOpenHelper.VERSION) - val database = openHelper.readableDatabase - databaseAdapter = UnencryptedDatabaseAdapter(database, openHelper.databaseName) + return try { + FileUtils.unzipFiles(file, getWorkingDir()) - if (database.version > BaseDatabaseOpenHelper.VERSION) { + if (!importMetadataFile.exists() || !importDatabaseFile.exists()) { throw d2ErrorBuilder - .errorDescription("Import database version higher than supported") - .errorCode(D2ErrorCode.DATABASE_IMPORT_VERSION_HIGHER_THAN_SUPPORTED) + .errorDescription("Import file is not valid") + .errorCode(D2ErrorCode.DATABASE_IMPORT_INVALID_FILE) .build() } - val userStore = UserStoreImpl(databaseAdapter) - val username = userStore.selectFirst()!!.username() - - val systemInfoStore = SystemInfoStoreImpl(databaseAdapter) - val contextPath = systemInfoStore.selectFirst()!!.contextPath()!! - val serverUrl = ServerUrlParser.parse(contextPath).toString() - - // TODO What to do if username is null? - val databaseName = nameGenerator.getDatabaseName(serverUrl, username!!, false) - - if (!context.databaseList().contains(databaseName)) { - val destDatabase = context.getDatabasePath(databaseName) - file.copyTo(destDatabase) - - multiUserDatabaseManager.createNew(serverUrl, username, false) - } else { - throw d2ErrorBuilder - .errorDescription("Import database already exists") - .errorCode(D2ErrorCode.DATABASE_IMPORT_ALREADY_EXISTS) - .build() + val metadataContent = importMetadataFile.readText(Charsets.UTF_8) + val metadata = objectMapper().readValue(metadataContent, DatabaseExportMetadata::class.java) + + when { + metadata.version > BaseDatabaseOpenHelper.VERSION -> + throw d2ErrorBuilder + .errorDescription("Import database version higher than supported") + .errorCode(D2ErrorCode.DATABASE_IMPORT_VERSION_HIGHER_THAN_SUPPORTED) + .build() + + getExistingAccountForMetadata(metadata) != null -> + throw d2ErrorBuilder + .errorDescription("Import database already exists") + .errorCode(D2ErrorCode.DATABASE_IMPORT_ALREADY_EXISTS) + .build() + + else -> { + val databaseAccount = multiUserDatabaseManager.createNewPendingToImport(metadata) + val destDatabase = context.getDatabasePath(databaseAccount.importDB()!!.protectedDbName()) + importDatabaseFile.copyTo(destDatabase) + + metadata + } + } + } catch (e: Exception) { + when (e) { + is D2Error -> throw e + else -> + throw d2ErrorBuilder + .errorDescription("Import database failed") + .errorCode(D2ErrorCode.DATABASE_IMPORT_FAILED) + .originalException(e) + .build() } } finally { - databaseAdapter?.close() - context.deleteDatabase(TmpDatabase) + importMetadataFile.delete() + importDatabaseFile.delete() } } override fun exportLoggedUserDatabase(): File { - context.deleteDatabase(ExportDatabase) + val exportMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.delete() } + val copiedDatabase = getWorkingDir().resolve(ExportDatabase).also { it.delete() } + val protectedDatabase = getWorkingDir().resolve(ExportDatabaseProtected).also { it.delete() } + val zipFile = getWorkingDir().resolve(ExportZip).also { it.delete() } if (!userModule.blockingIsLogged()) { throw d2ErrorBuilder @@ -138,12 +141,10 @@ internal class DatabaseImportExportImpl( } val credentials = credentialsStore.get() - val databasesConfiguration = databaseConfigurationSecureStore.get() - val userConfiguration = DatabaseConfigurationHelper.getLoggedAccount( - configuration = databasesConfiguration, + val userConfiguration = multiUserDatabaseManager.getAccount( username = credentials.username, serverUrl = credentials.serverUrl, - ) + )!! if (userConfiguration.encrypted()) { throw d2ErrorBuilder @@ -155,24 +156,51 @@ internal class DatabaseImportExportImpl( databaseAdapter.close() val databaseName = userConfiguration.databaseName() - val copiedDatabase = databaseRenamer.copyDatabase(databaseName, ExportDatabase) + val databaseFile = getDatabaseFile(databaseName) + databaseFile.copyTo(copiedDatabase) + CipherUtil.encryptFileUsingCredentials( + input = copiedDatabase, + output = protectedDatabase, + username = credentials.username, + password = credentials.password!!, + ) val metadata = DatabaseExportMetadata( - version = "V1", + version = BaseDatabaseOpenHelper.VERSION, date = Date().simpleDateFormat()!!, - serverUrl = credentials.serverUrl, - username = credentials.username, + serverUrl = userConfiguration.serverUrl(), + username = userConfiguration.username(), + encrypted = userConfiguration.encrypted(), ) - val exportMetadataPath = copiedDatabase.parentFile?.let { "${it.path}/${ExportMetadata}".toPath() } - FileSystem.SYSTEM.sink(exportMetadataPath!!).use { sinkFile -> - sinkFile.buffer().use { bufferedSinkFile -> - bufferedSinkFile.writeUtf8(objectMapper().writeValueAsString(metadata)) - } + exportMetadataFile.bufferedWriter(Charsets.UTF_8).use { + it.write(objectMapper().writeValueAsString(metadata)) } - FileUtils.zipFiles(exportMetadataPath, exportMetadataPath.parent!!.resolve("zipped.zip")) + FileUtils.zipFiles( + files = listOf(exportMetadataFile, protectedDatabase), + zipFile = zipFile, + ) + + exportMetadataFile.delete() + copiedDatabase.delete() + protectedDatabase.delete() + + return zipFile + } - return copiedDatabase + private fun getWorkingDir(): File { + return context.filesDir + } + + private fun getDatabaseFile(dbName: String): File { + return context.getDatabasePath(dbName) + } + + private fun getExistingAccountForMetadata(metadata: DatabaseExportMetadata): DatabaseAccount? { + return multiUserDatabaseManager.getAccount( + metadata.serverUrl, + metadata.username, + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccount.java b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccount.java index 032192d5f7..36112df0fb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccount.java +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccount.java @@ -64,6 +64,10 @@ public abstract class DatabaseAccount { @Nullable public abstract State syncState(); + @JsonProperty() + @Nullable + public abstract DatabaseAccountImportDB importDB(); + public abstract Builder toBuilder(); public static Builder builder() { @@ -84,6 +88,8 @@ public abstract static class Builder { public abstract Builder databaseCreationDate(String databaseCreationDate); + public abstract Builder importDB(DatabaseAccountImportDB importDB); + public abstract Builder syncState(State syncState); public abstract DatabaseAccount build(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportDB.java b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportDB.java new file mode 100644 index 0000000000..2e39a8c7f0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportDB.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.configuration.internal; + +import androidx.annotation.NonNull; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +@AutoValue +@JsonDeserialize(builder = AutoValue_DatabaseAccountImportDB.Builder.class) +public abstract class DatabaseAccountImportDB { + + @JsonProperty() + @NonNull + public abstract DatabaseAccountImportStatus status(); + + @JsonProperty + @NonNull + public abstract String protectedDbName(); + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_DatabaseAccountImportDB.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder status(DatabaseAccountImportStatus importStatus); + + public abstract Builder protectedDbName(String protectedDbName); + + public abstract DatabaseAccountImportDB build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportStatus.kt b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportStatus.kt new file mode 100644 index 0000000000..29ef296726 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseAccountImportStatus.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.configuration.internal + +enum class DatabaseAccountImportStatus { + PENDING_TO_IMPORT, + IMPORTED, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseConfigurationHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseConfigurationHelper.kt index a0ca76bbf8..23194c1763 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseConfigurationHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/DatabaseConfigurationHelper.kt @@ -48,26 +48,42 @@ internal class DatabaseConfigurationHelper( .build() } - fun addAccount( + fun addOrUpdateAccount( configuration: DatabasesConfiguration?, serverUrl: String, username: String, encrypt: Boolean, + importStatus: DatabaseAccountImportStatus? = null, ): DatabasesConfiguration { + val dbName = databaseNameGenerator.getDatabaseName(serverUrl, username, encrypt) + val importDb = importStatus?.let { + DatabaseAccountImportDB.builder() + .status(importStatus) + .protectedDbName("$dbName.protected") + .build() + } val newAccount = DatabaseAccount.builder() .username(username) .serverUrl(serverUrl) - .databaseName(databaseNameGenerator.getDatabaseName(serverUrl, username, encrypt)) + .databaseName(dbName) .encrypted(encrypt) .databaseCreationDate(dateProvider.dateStr) + .importDB(importDb) .build() + return addOrUpdateAccount(configuration, newAccount) + } + + fun addOrUpdateAccount( + configuration: DatabasesConfiguration?, + account: DatabaseAccount, + ): DatabasesConfiguration { val otherAccounts = configuration?.accounts()?.filterNot { - equalsIgnoreProtocol(it.serverUrl(), serverUrl) && it.username() == username + equalsIgnoreProtocol(it.serverUrl(), account.serverUrl()) && it.username() == account.username() } ?: emptyList() return (configuration?.toBuilder() ?: DatabasesConfiguration.builder()) - .accounts(otherAccounts + newAccount) + .accounts(otherAccounts + account) .build() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt index d890eea172..c8ff490174 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt @@ -30,13 +30,16 @@ package org.hisp.dhis.android.core.configuration.internal import android.content.Context import android.util.Log import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata import org.hisp.dhis.android.core.arch.db.access.internal.DatabaseAdapterFactory import org.hisp.dhis.android.core.arch.db.access.internal.DatabaseExport import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import org.hisp.dhis.android.core.arch.storage.internal.Credentials +import org.hisp.dhis.android.core.util.CipherUtil import org.koin.core.annotation.Singleton @Singleton +@Suppress("TooManyFunctions") internal class MultiUserDatabaseManager( private val context: Context, private val databaseAdapter: DatabaseAdapter, @@ -76,26 +79,21 @@ internal class MultiUserDatabaseManager( } fun createNew(serverUrl: String, username: String, encrypt: Boolean) { - val configuration = databaseConfigurationSecureStore.get() - - configuration?.maxAccounts()?.let { maxAccounts -> - val exceedingAccounts = DatabaseConfigurationHelper - .getOldestAccounts(configuration.accounts(), maxAccounts - 1) - - val updatedConfiguration = - DatabaseConfigurationHelper.removeAccount(configuration, exceedingAccounts) - - databaseConfigurationSecureStore.set(updatedConfiguration) - exceedingAccounts.forEach { - FileResourceDirectoryHelper.deleteFileResourceDirectories(context, it) - databaseAdapterFactory.deleteDatabase(it) - } - } - - val userConfiguration = addNewAccountInternal(serverUrl, username, encrypt) + removeExceedingAccounts() + val userConfiguration = addOrUpdateAccountInternal(serverUrl, username, encrypt) databaseAdapterFactory.createOrOpenDatabase(databaseAdapter, userConfiguration) } + fun createNewPendingToImport(metadata: DatabaseExportMetadata): DatabaseAccount { + removeExceedingAccounts() + return addOrUpdateAccountInternal( + metadata.serverUrl, + metadata.username, + metadata.encrypted, + importStatus = DatabaseAccountImportStatus.PENDING_TO_IMPORT, + ) + } + fun changeEncryptionIfRequired(credentials: Credentials, encrypt: Boolean) { loadExistingChangingEncryptionIfRequired( credentials.serverUrl, @@ -126,6 +124,28 @@ internal class MultiUserDatabaseManager( } } + @Suppress("TooGenericExceptionCaught") + fun importAndLoadDb(account: DatabaseAccount, password: String) { + val protectedDbPath = context.getDatabasePath(account.importDB()!!.protectedDbName()) + val dbPath = context.getDatabasePath(account.databaseName()) + try { + CipherUtil.decryptFileUsingCredentials(protectedDbPath, dbPath, account.username(), password) + protectedDbPath.delete() + databaseAdapterFactory.createOrOpenDatabase(databaseAdapter, account) + val importedAccount = account.toBuilder() + .importDB( + account.importDB()!!.toBuilder() + .status(DatabaseAccountImportStatus.IMPORTED) + .build(), + ) + .build() + addOrUpdatedAccountInternal(importedAccount) + } catch (e: Exception) { + dbPath.delete() + throw e + } + } + private fun loadExistingChangingEncryptionIfRequired( serverUrl: String, username: String, @@ -136,7 +156,7 @@ internal class MultiUserDatabaseManager( val encrypt = encryptionExtractor(existingAccount) changeEncryptionIfRequired(serverUrl, existingAccount, encrypt) if (encrypt != existingAccount.encrypted() || alsoOpenWhenEncryptionDoesntChange) { - val updatedAccount = addNewAccountInternal( + val updatedAccount = addOrUpdateAccountInternal( serverUrl, username, encrypt, @@ -165,26 +185,54 @@ internal class MultiUserDatabaseManager( } } - private fun getAccount(serverUrl: String, username: String): DatabaseAccount? { + fun getAccount(serverUrl: String, username: String): DatabaseAccount? { val configuration = databaseConfigurationSecureStore.get() return DatabaseConfigurationHelper.getAccount(configuration, serverUrl, username) } - private fun addNewAccountInternal( + private fun addOrUpdateAccountInternal( serverUrl: String, username: String, encrypt: Boolean, + importStatus: DatabaseAccountImportStatus? = null, ): DatabaseAccount { - val updatedAccount = configurationHelper.addAccount( + val updatedAccount = configurationHelper.addOrUpdateAccount( databaseConfigurationSecureStore.get(), serverUrl, username, encrypt, + importStatus, ) databaseConfigurationSecureStore.set(updatedAccount) return DatabaseConfigurationHelper.getLoggedAccount(updatedAccount, username, serverUrl) } + private fun addOrUpdatedAccountInternal(account: DatabaseAccount) { + val updatedAccount = configurationHelper.addOrUpdateAccount( + databaseConfigurationSecureStore.get(), + account, + ) + databaseConfigurationSecureStore.set(updatedAccount) + } + + private fun removeExceedingAccounts() { + val configuration = databaseConfigurationSecureStore.get() + + configuration?.maxAccounts()?.let { maxAccounts -> + val exceedingAccounts = DatabaseConfigurationHelper + .getOldestAccounts(configuration.accounts(), maxAccounts - 1) + + val updatedConfiguration = + DatabaseConfigurationHelper.removeAccount(configuration, exceedingAccounts) + + databaseConfigurationSecureStore.set(updatedConfiguration) + exceedingAccounts.forEach { + FileResourceDirectoryHelper.deleteFileResourceDirectories(context, it) + databaseAdapterFactory.deleteDatabase(it) + } + } + } + companion object { const val DefaultMaxAccounts = 1 internal val DefaultTestMaxAccounts = null diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java index b62192d085..2eafbfa6fc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java @@ -45,6 +45,8 @@ public enum D2ErrorCode { DATABASE_EXPORT_LOGIN_FIRST, DATABASE_EXPORT_ENCRYPTED_NOT_SUPPORTED, DATABASE_IMPORT_ALREADY_EXISTS, + DATABASE_IMPORT_FAILED, + DATABASE_IMPORT_INVALID_FILE, DATABASE_IMPORT_LOGOUT_FIRST, DATABASE_IMPORT_VERSION_HIGHER_THAN_SUPPORTED, FILE_NOT_FOUND, diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.kt similarity index 79% rename from core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java rename to core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.kt index f78d89ff32..28e84ce27b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/MaintenanceModule.kt @@ -25,16 +25,17 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.android.core.maintenance; +package org.hisp.dhis.android.core.maintenance -import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport; +import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport -public interface MaintenanceModule { - ForeignKeyViolationCollectionRepository foreignKeyViolations(); - D2ErrorCollectionRepository d2Errors(); - PerformanceHintsService getPerformanceHintsService(int organisationUnitThreshold, - int programRulesPerProgramThreshold); +interface MaintenanceModule { + fun foreignKeyViolations(): ForeignKeyViolationCollectionRepository + fun d2Errors(): D2ErrorCollectionRepository + fun getPerformanceHintsService( + organisationUnitThreshold: Int, + programRulesPerProgramThreshold: Int, + ): PerformanceHintsService - - DatabaseImportExport databaseImportExport(); -} \ No newline at end of file + fun databaseImportExport(): DatabaseImportExport +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt index 29cc2ea43c..8a765801e3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt @@ -79,13 +79,17 @@ internal class LogInCall( val credentials = Credentials(username!!, trimmedServerUrl!!, password, null) return try { - val user = coroutineAPICallExecutor.wrap(errorCatcher = apiCallErrorCatcher) { - userService.authenticate( - okhttp3.Credentials.basic(username, password!!), - UserFields.allFieldsWithoutOrgUnit(null), - ) - }.getOrThrow() - loginOnline(user, credentials) + if (databaseManager.isPendingToImportDB(trimmedServerUrl, username)) { + importDB(trimmedServerUrl, credentials) + } else { + val user = coroutineAPICallExecutor.wrap(errorCatcher = apiCallErrorCatcher) { + userService.authenticate( + okhttp3.Credentials.basic(username, password!!), + UserFields.allFieldsWithoutOrgUnit(null), + ) + }.getOrThrow() + loginOnline(user, credentials) + } } catch (d2Error: D2Error) { if (d2Error.isOffline) { tryLoginOffline(credentials, d2Error) @@ -160,6 +164,19 @@ internal class LogInCall( return userStore.selectByUid(existingUser.user()!!)!! } + @Suppress("TooGenericExceptionCaught") + private fun importDB(serverUrl: String, credentials: Credentials): User { + try { + databaseManager.importDB(serverUrl, credentials) + credentialsSecureStore.set(credentials) + val existingUser = authenticatedUserStore.selectFirst() ?: throw exceptions.noUserOfflineError() + userIdStore.set(existingUser.user()!!) + return userStore.selectByUid(existingUser.user()!!)!! + } catch (e: Exception) { + throw exceptions.badCredentialsError() + } + } + @Throws(D2Error::class) suspend fun blockingLogInOpenIDConnect(serverUrl: String, openIDConnectState: AuthState): User { val trimmedServerUrl = ServerUrlParser.trimAndRemoveTrailingSlash(serverUrl) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInDatabaseManager.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInDatabaseManager.kt index c99f61d65b..5345ae6e35 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInDatabaseManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInDatabaseManager.kt @@ -27,6 +27,8 @@ */ package org.hisp.dhis.android.core.user.internal +import org.hisp.dhis.android.core.arch.storage.internal.Credentials +import org.hisp.dhis.android.core.configuration.internal.DatabaseAccountImportStatus import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManager import org.hisp.dhis.android.core.settings.internal.GeneralSettingCall import org.koin.core.annotation.Singleton @@ -60,4 +62,14 @@ internal class LogInDatabaseManager( username, ) } + + fun isPendingToImportDB(serverUrl: String, username: String): Boolean { + val existingAccount = multiUserDatabaseManager.getAccount(serverUrl, username) + return existingAccount?.importDB()?.status() == DatabaseAccountImportStatus.PENDING_TO_IMPORT + } + + fun importDB(serverUrl: String, credentials: Credentials) { + val existingAccount = multiUserDatabaseManager.getAccount(serverUrl, credentials.username)!! + multiUserDatabaseManager.importAndLoadDb(existingAccount, credentials.password!!) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/CipherUtil.kt b/core/src/main/java/org/hisp/dhis/android/core/util/CipherUtil.kt new file mode 100644 index 0000000000..d75b267e00 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/util/CipherUtil.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +import java.io.File +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.spec.KeySpec +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +@Suppress("NestedBlockDepth", "MagicNumber") +internal object CipherUtil { + fun encryptFileUsingCredentials(input: File, output: File, username: String, password: String) { + val cipher = getCipher(Cipher.ENCRYPT_MODE, username, password) + applyCipher(cipher, input, output) + } + + fun decryptFileUsingCredentials(input: File, output: File, username: String, password: String) { + val cipher = getCipher(Cipher.DECRYPT_MODE, username, password) + applyCipher(cipher, input, output) + } + + private fun applyCipher(cipher: Cipher, input: File, output: File) { + input.inputStream().use { inputStream -> + output.outputStream().use { outputStream -> + val buffer = ByteArray(64) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + cipher.update(buffer, 0, bytesRead)?.let { outputStream.write(it) } + } + cipher.doFinal()?.let { outputStream.write(it) } + } + } + } + + private fun getCipher(mode: Int, username: String, password: String): Cipher { + val iv: ByteArray = getSalt(username) + val aesKey: SecretKey = getAESKeyFromPassword(password, iv) + val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") + cipher.init(mode, aesKey, IvParameterSpec(iv)) + + return cipher + } + + internal fun getSalt(string: String): ByteArray { + val md = MessageDigest.getInstance("MD5") + md.reset() + md.update(string.toByteArray((StandardCharsets.UTF_8))) + return md.digest().slice(IntRange(0, 15)).toByteArray() + } + + private fun getAESKeyFromPassword(password: String, salt: ByteArray?): SecretKey { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val iterationCount = 10000 + val keyLength = 256 + val spec: KeySpec = PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength) + return SecretKeySpec(factory.generateSecret(spec).encoded, "AES") + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt index 3f6d724595..c936d5c127 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt @@ -28,19 +28,74 @@ package org.hisp.dhis.android.core.util -import okio.FileSystem -import okio.GzipSink -import okio.Okio -import okio.Path -import okio.buffer -import okio.use +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream +@Suppress("NestedBlockDepth") internal object FileUtils { - fun zipFiles(file: Path, target: Path) { - FileSystem.SYSTEM.sink(target).use { sink -> - GzipSink(sink).buffer().use { gzipBuffer -> - gzipBuffer.writeAll(FileSystem.SYSTEM.source(file)) + + private const val bufferSize = 1024 + + @Suppress("TooGenericExceptionCaught") + fun zipFiles(files: List, zipFile: File) { + val buffer = ByteArray(bufferSize) + + try { + val fos = FileOutputStream(zipFile.path) + val zos = ZipOutputStream(fos) + + files.forEach { file -> + val ze = ZipEntry(file.name) + zos.putNextEntry(ze) + val inStream = FileInputStream(file) + while (true) { + val len = inStream.read(buffer) + if (len <= 0) break + zos.write(buffer, 0, len) + } + + zos.closeEntry() + inStream.close() + } + + zos.close() + fos.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun unzipFiles(zipFile: File, unzipDirectory: File) { + val zip = ZipFile(zipFile) + val enum = zip.entries() + while (enum.hasMoreElements()) { + val entry = enum.nextElement() + val entryName = entry.name + val fis = FileInputStream(zip.name) + val zis = ZipInputStream(fis) + + while (true) { + val nextEntry = zis.nextEntry ?: break + if (nextEntry.name == entryName) { + val fout = FileOutputStream(File(unzipDirectory, nextEntry.name)) + var c = zis.read() + while (c != -1) { + fout.write(c) + c = zis.read() + } + zis.closeEntry() + fout.close() + } } + + zis.close() + fis.close() } + zip.close() } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java b/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java deleted file mode 100644 index 45e9ec6753..0000000000 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/CipherTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2004-2024, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.analytics; - -import org.junit.Test; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Base64; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -public class CipherTest { - - private static byte[] salt = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; - - private static String encryptMessageGH(String message, String password) throws Exception { - byte[] iv = salt; - SecretKey aesKey = getAESKeyFromPassword(password, iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); - cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv)); - byte[] ciphertext = cipher.doFinal(message.getBytes()); - byte[] encrypted = new byte[iv.length + ciphertext.length]; - System.arraycopy(iv, 0, encrypted, 0, iv.length); - System.arraycopy(ciphertext, 0, encrypted, iv.length, ciphertext.length); - return Base64.getEncoder().encodeToString(encrypted); - } - - private static String decryptMessageGH(String encryptedMessage, String password) throws Exception { - byte[] encrypted = Base64.getDecoder().decode(encryptedMessage); - byte[] iv = salt; - SecretKey aesKey = getAESKeyFromPassword(password, iv); - System.arraycopy(encrypted, 0, iv, 0, iv.length); - byte[] ciphertext = new byte[encrypted.length - iv.length]; - System.arraycopy(encrypted, iv.length, ciphertext, 0, ciphertext.length); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); - cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv)); - return new String(cipher.doFinal(ciphertext), "UTF-8"); - } - - public static SecretKey getAESKeyFromPassword(String password, byte[] salt) - throws NoSuchAlgorithmException, InvalidKeySpecException { - - long start = System.currentTimeMillis(); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - // iterationCount = 1000 - // keyLength = 256 - int ITERATION_COUNT = 100000; - int keyLength = 256; - KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, keyLength); - SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); - - long end = System.currentTimeMillis(); - - System.out.println("Key generated in " + (end - start)); - - return key; - } - - @Test - public void cipher_test() throws Exception { - String orig = "Test message"; - String enc = encryptMessageGH(orig, "abcdef1234"); - System.out.println("Encrypted: " + enc); - String dec = decryptMessageGH(enc, "abcdef1234"); - System.out.println("Decrypted: " + dec); - } -} diff --git a/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/DatabasesConfigurationHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/DatabasesConfigurationHelperShould.kt index 292321b03c..bad191ce75 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/DatabasesConfigurationHelperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/DatabasesConfigurationHelperShould.kt @@ -124,19 +124,19 @@ class DatabasesConfigurationHelperShould { @Test fun add_new_configuration_to_empty() { - val config = helper.addAccount(null, url1, username1, false) + val config = helper.addOrUpdateAccount(null, url1, username1, false) assertThat(config).isEqualTo(singleServerSingleUserConfig) } @Test fun add_new_configuration_to_single_server_single_user_in_same_server() { - val config = helper.addAccount(singleServerSingleUserConfig, url1, username2, false) + val config = helper.addOrUpdateAccount(singleServerSingleUserConfig, url1, username2, false) assertThat(config).isEqualTo(singleServer2UserConfig) } @Test fun add_new_configuration_to_single_server_single_user_in_other_server() { - val config = helper.addAccount(singleServerSingleUserConfig, url2, username2, false) + val config = helper.addOrUpdateAccount(singleServerSingleUserConfig, url2, username2, false) assertThat( DatabaseConfigurationHelper .getLoggedAccount(config, username2, url2), diff --git a/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManagerUnitShould.kt b/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManagerUnitShould.kt index 3ceeb14bc4..641b754a6f 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManagerUnitShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManagerUnitShould.kt @@ -96,7 +96,7 @@ class MultiUserDatabaseManagerUnitShould : BaseCallShould() { @Test fun create_new_db_when_no_previous_configuration_on_loadExistingChangingEncryptionIfRequiredOtherwiseCreateNew() { val encrypt = false - whenever(configurationHelper.addAccount(null, serverUrl, username, encrypt)) + whenever(configurationHelper.addOrUpdateAccount(null, serverUrl, username, encrypt)) .doReturn(unencryptedConfiguration) manager.loadExistingChangingEncryptionIfRequiredOtherwiseCreateNew(serverUrl, username, encrypt) @@ -108,7 +108,7 @@ class MultiUserDatabaseManagerUnitShould : BaseCallShould() { fun copy_database_when_changing_encryption_on_loadExistingChangingEncryptionIfRequiredOtherwiseCreateNew() { val encrypt = true whenever(databaseConfigurationSecureStore.get()).doReturn(unencryptedConfiguration) - whenever(configurationHelper.addAccount(unencryptedConfiguration, serverUrl, username, encrypt)) + whenever(configurationHelper.addOrUpdateAccount(unencryptedConfiguration, serverUrl, username, encrypt)) .doReturn(encryptedConfiguration) manager.loadExistingChangingEncryptionIfRequiredOtherwiseCreateNew(serverUrl, username, encrypt) @@ -127,7 +127,7 @@ class MultiUserDatabaseManagerUnitShould : BaseCallShould() { @Test fun open_database_when_existing_when_calling_loadExistingKeepingEncryption() { whenever(databaseConfigurationSecureStore.get()).doReturn(unencryptedConfiguration) - whenever(configurationHelper.addAccount(unencryptedConfiguration, serverUrl, username, false)) + whenever(configurationHelper.addOrUpdateAccount(unencryptedConfiguration, serverUrl, username, false)) .doReturn(unencryptedConfiguration) manager.loadExistingKeepingEncryption(serverUrl, username) @@ -154,7 +154,7 @@ class MultiUserDatabaseManagerUnitShould : BaseCallShould() { val newUsername = "new_username" val newServerUrl = "new_server_url" val newConfiguration = buildUserConfiguration(newUsername, "2021-06-01T00:01:04.000", newServerUrl) - whenever(configurationHelper.addAccount(configuration, newServerUrl, newUsername, false)) + whenever(configurationHelper.addOrUpdateAccount(configuration, newServerUrl, newUsername, false)) .doReturn(DatabasesConfiguration.builder().accounts(listOf(newConfiguration)).build()) manager.createNew(newServerUrl, newUsername, false) diff --git a/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt b/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt new file mode 100644 index 0000000000..8b8e9fcc40 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CipherUtilShould { + @Test + fun create_16_long_salt() { + assertThat(CipherUtil.getSalt("password").size).isEqualTo(16) + assertThat(CipherUtil.getSalt("s").size).isEqualTo(16) + assertThat(CipherUtil.getSalt("veryverylongpasswordforuser").size).isEqualTo(16) + } +} From b8b552d9cea89b183a6dc21583a4c23920f69ae8 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 23 Jan 2024 15:50:18 +0100 Subject: [PATCH 035/222] [ANDROSDK-1218] Remove database.close() request --- .../core/arch/db/access/internal/DatabaseImportExportImpl.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index 8d1d77e194..f2ed5cbaa4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -28,7 +28,6 @@ package org.hisp.dhis.android.core.arch.db.access.internal import android.content.Context -import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory.objectMapper @@ -52,7 +51,6 @@ internal class DatabaseImportExportImpl( private val multiUserDatabaseManager: MultiUserDatabaseManager, private val userModule: UserModule, private val credentialsStore: CredentialsSecureStore, - private val databaseAdapter: DatabaseAdapter, ) : DatabaseImportExport { companion object { @@ -153,8 +151,6 @@ internal class DatabaseImportExportImpl( .build() } - databaseAdapter.close() - val databaseName = userConfiguration.databaseName() val databaseFile = getDatabaseFile(databaseName) databaseFile.copyTo(copiedDatabase) From 09ae36fac5ade965411c1757a6c7dd097349a781 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 24 Jan 2024 09:52:28 +0100 Subject: [PATCH 036/222] [ANDROSDK-1218] Increse bufferSize for unzipping --- .../org/hisp/dhis/android/core/util/FileUtils.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt index c936d5c127..f53a37cadd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/util/FileUtils.kt @@ -39,7 +39,7 @@ import java.util.zip.ZipOutputStream @Suppress("NestedBlockDepth") internal object FileUtils { - private const val bufferSize = 1024 + private const val bufferSize = 512 @Suppress("TooGenericExceptionCaught") fun zipFiles(files: List, zipFile: File) { @@ -71,6 +71,8 @@ internal object FileUtils { } fun unzipFiles(zipFile: File, unzipDirectory: File) { + val buffer = ByteArray(bufferSize) + val zip = ZipFile(zipFile) val enum = zip.entries() while (enum.hasMoreElements()) { @@ -83,11 +85,12 @@ internal object FileUtils { val nextEntry = zis.nextEntry ?: break if (nextEntry.name == entryName) { val fout = FileOutputStream(File(unzipDirectory, nextEntry.name)) - var c = zis.read() - while (c != -1) { - fout.write(c) - c = zis.read() + while (true) { + val len = zis.read(buffer) + if (len <= 0) break + fout.write(buffer, 0, len) } + zis.closeEntry() fout.close() } From a271a11b246cbbbf0314005f09010784fb618ed8 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 24 Jan 2024 10:08:25 +0100 Subject: [PATCH 037/222] [ANDROSDK-1218] Throw an error if file cannot be deleted --- .../internal/DatabaseImportExportImpl.kt | 23 ++++----- .../internal/MultiUserDatabaseManager.kt | 5 +- .../dhis/android/core/util/FileExtensions.kt | 47 +++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/util/FileExtensions.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index f2ed5cbaa4..515ea74ac7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -40,6 +40,7 @@ import org.hisp.dhis.android.core.maintenance.D2ErrorComponent import org.hisp.dhis.android.core.user.UserModule import org.hisp.dhis.android.core.util.CipherUtil import org.hisp.dhis.android.core.util.FileUtils +import org.hisp.dhis.android.core.util.deleteIfExists import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton import java.io.File @@ -72,8 +73,8 @@ internal class DatabaseImportExportImpl( .build() } - val importMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.delete() } - val importDatabaseFile = getWorkingDir().resolve(ExportDatabaseProtected).also { it.delete() } + val importMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.deleteIfExists() } + val importDatabaseFile = getWorkingDir().resolve(ExportDatabaseProtected).also { it.deleteIfExists() } return try { FileUtils.unzipFiles(file, getWorkingDir()) @@ -120,16 +121,16 @@ internal class DatabaseImportExportImpl( .build() } } finally { - importMetadataFile.delete() - importDatabaseFile.delete() + importMetadataFile.deleteIfExists() + importDatabaseFile.deleteIfExists() } } override fun exportLoggedUserDatabase(): File { - val exportMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.delete() } - val copiedDatabase = getWorkingDir().resolve(ExportDatabase).also { it.delete() } - val protectedDatabase = getWorkingDir().resolve(ExportDatabaseProtected).also { it.delete() } - val zipFile = getWorkingDir().resolve(ExportZip).also { it.delete() } + val exportMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.deleteIfExists() } + val copiedDatabase = getWorkingDir().resolve(ExportDatabase).also { it.deleteIfExists() } + val protectedDatabase = getWorkingDir().resolve(ExportDatabaseProtected).also { it.deleteIfExists() } + val zipFile = getWorkingDir().resolve(ExportZip).also { it.deleteIfExists() } if (!userModule.blockingIsLogged()) { throw d2ErrorBuilder @@ -178,9 +179,9 @@ internal class DatabaseImportExportImpl( zipFile = zipFile, ) - exportMetadataFile.delete() - copiedDatabase.delete() - protectedDatabase.delete() + exportMetadataFile.deleteIfExists() + copiedDatabase.deleteIfExists() + protectedDatabase.deleteIfExists() return zipFile } diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt index c8ff490174..df152a639a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt @@ -36,6 +36,7 @@ import org.hisp.dhis.android.core.arch.db.access.internal.DatabaseExport import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import org.hisp.dhis.android.core.arch.storage.internal.Credentials import org.hisp.dhis.android.core.util.CipherUtil +import org.hisp.dhis.android.core.util.deleteIfExists import org.koin.core.annotation.Singleton @Singleton @@ -130,7 +131,7 @@ internal class MultiUserDatabaseManager( val dbPath = context.getDatabasePath(account.databaseName()) try { CipherUtil.decryptFileUsingCredentials(protectedDbPath, dbPath, account.username(), password) - protectedDbPath.delete() + protectedDbPath.deleteIfExists() databaseAdapterFactory.createOrOpenDatabase(databaseAdapter, account) val importedAccount = account.toBuilder() .importDB( @@ -141,7 +142,7 @@ internal class MultiUserDatabaseManager( .build() addOrUpdatedAccountInternal(importedAccount) } catch (e: Exception) { - dbPath.delete() + dbPath.deleteIfExists() throw e } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/FileExtensions.kt b/core/src/main/java/org/hisp/dhis/android/core/util/FileExtensions.kt new file mode 100644 index 0000000000..ecf02895fa --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/util/FileExtensions.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.maintenance.D2ErrorCode +import org.hisp.dhis.android.core.maintenance.D2ErrorComponent +import java.io.File + +internal fun File.deleteIfExists() { + if (exists()) { + val deleted = delete() + if (!deleted) { + throw D2Error.builder() + .errorComponent(D2ErrorComponent.SDK) + .errorDescription("File $path exists and cannot be deleted") + .errorCode(D2ErrorCode.UNEXPECTED) + .build() + } + } +} From 3d55ab481a02ca63bad7f6e622fa3c044115fc69 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 25 Jan 2024 11:12:59 +0100 Subject: [PATCH 038/222] [ANDROSDK-1805] Adapt to changes in tracker payload (v41) --- .../{NTIPayload.kt => TrackerPager.kt} | 9 +-- .../api/payload/internal/TrackerPayload.kt | 59 +++++++++++++++++++ .../internal/NewEventEndpointCallFactory.kt | 6 +- .../NewTrackedEntityEndpointCallFactory.kt | 8 +-- .../exporter/TrackerExporterService.kt | 8 +-- ...ker_importer_events_greater_equal_v41.json | 35 +++++++++++ ...new_tracker_importer_events_lower_v41.json | 31 ++++++++++ ...er_tracked_entities_greater_equal_v41.json | 34 +++++++++++ ...r_importer_tracked_entities_lower_v41.json | 30 ++++++++++ ...porterEventPayloadGreaterEqualV41Should.kt | 52 ++++++++++++++++ ...ackerImporterEventPayloadLowerV41Should.kt | 52 ++++++++++++++++ ...ackedEntityPayloadGreaterEqualV41Should.kt | 52 ++++++++++++++++ ...orterTrackedEntityPayloadLowerV41Should.kt | 52 ++++++++++++++++ 13 files changed, 413 insertions(+), 15 deletions(-) rename core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/{NTIPayload.kt => TrackerPager.kt} (91%) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPayload.kt create mode 100644 core/src/sharedTest/resources/event/new_tracker_importer_events_greater_equal_v41.json create mode 100644 core/src/sharedTest/resources/event/new_tracker_importer_events_lower_v41.json create mode 100644 core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_greater_equal_v41.json create mode 100644 core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_lower_v41.json create mode 100644 core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadGreaterEqualV41Should.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadLowerV41Should.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadGreaterEqualV41Should.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadLowerV41Should.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/NTIPayload.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPager.kt similarity index 91% rename from core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/NTIPayload.kt rename to core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPager.kt index fbd7e96ad0..21afed63a9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/NTIPayload.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPager.kt @@ -27,8 +27,9 @@ */ package org.hisp.dhis.android.core.arch.api.payload.internal -data class NTIPayload( - val page: Int, - val pageSize: Int, - val instances: List, +import com.fasterxml.jackson.annotation.JsonProperty + +internal data class TrackerPager( + @JsonProperty val page: Int, + @JsonProperty val pageSize: Int, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPayload.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPayload.kt new file mode 100644 index 0000000000..97d7c47eb6 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/payload/internal/TrackerPayload.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.api.payload.internal + +import com.fasterxml.jackson.annotation.JsonAnySetter +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty + +internal data class TrackerPayload( + @JsonProperty private val pager: TrackerPager?, + @JsonProperty private val page: Int?, + @JsonProperty private val pageSize: Int?, + @JsonIgnore private var items: List = emptyList(), +) { + + @JsonAnySetter + @Suppress("unused") + private fun processItems(key: String?, values: List) { + items = values + } + + fun pager(): TrackerPager? { + return pager + ?: if (page != null && pageSize != null) { + TrackerPager(page = page, pageSize = pageSize) + } else { + null + } + } + + fun items(): List { + return items + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt index c25e7d2bb8..fb87a4ec06 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt @@ -27,8 +27,8 @@ */ package org.hisp.dhis.android.core.event.internal -import org.hisp.dhis.android.core.arch.api.payload.internal.NTIPayload import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.NewTrackerImporterEvent import org.hisp.dhis.android.core.event.NewTrackerImporterEventTransformer @@ -67,8 +67,8 @@ internal class NewEventEndpointCallFactory( ).let { mapPayload(it) } } - private fun mapPayload(payload: NTIPayload): Payload { - val newItems = payload.instances.map { t -> NewTrackerImporterEventTransformer.deTransform(t) } + private fun mapPayload(payload: TrackerPayload): Payload { + val newItems = payload.items().map { t -> NewTrackerImporterEventTransformer.deTransform(t) } return Payload(newItems) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index b0fe30d842..df9d7892bb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -28,8 +28,8 @@ package org.hisp.dhis.android.core.trackedentity.internal import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutor -import org.hisp.dhis.android.core.arch.api.payload.internal.NTIPayload import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload import org.hisp.dhis.android.core.event.NewTrackerImporterEvent import org.hisp.dhis.android.core.event.internal.NewEventFields import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode @@ -169,7 +169,7 @@ internal class NewTrackedEntityEndpointCallFactory( updatedBefore = query.lastUpdatedEndDate.simpleDateFormat(), includeDeleted = query.includeDeleted, ) - }.getOrThrow().instances + }.getOrThrow().items() } private suspend fun getTrackedEntityQuery(query: TrackedEntityInstanceQueryOnline): List { @@ -227,8 +227,8 @@ internal class NewTrackedEntityEndpointCallFactory( ) } - private fun mapPayload(payload: NTIPayload): Payload { - val newItems = payload.instances.map { t -> NewTrackerImporterTrackedEntityTransformer.deTransform(t) } + private fun mapPayload(payload: TrackerPayload): Payload { + val newItems = payload.items().map { t -> NewTrackerImporterTrackedEntityTransformer.deTransform(t) } return Payload(newItems) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index 2eeb9e63a1..3edbc71c88 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -29,7 +29,7 @@ package org.hisp.dhis.android.core.tracker.exporter import org.hisp.dhis.android.core.arch.api.fields.internal.Fields import org.hisp.dhis.android.core.arch.api.filters.internal.Which -import org.hisp.dhis.android.core.arch.api.payload.internal.NTIPayload +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload import org.hisp.dhis.android.core.enrollment.NewTrackerImporterEnrollment import org.hisp.dhis.android.core.event.NewTrackerImporterEvent import org.hisp.dhis.android.core.trackedentity.NewTrackerImporterTrackedEntity @@ -75,7 +75,7 @@ internal interface TrackerExporterService { @Query(PAGE) page: Int, @Query(PAGE_SIZE) pageSize: Int, @Query(INCLUDE_DELETED) includeDeleted: Boolean = false, - ): NTIPayload + ): TrackerPayload @GET("$ENROLLMENTS/{$ENROLLMENT}") suspend fun getEnrollmentSingle( @@ -112,14 +112,14 @@ internal interface TrackerExporterService { @Query(UPDATED_BEFORE) updatedBefore: String? = null, @Query(INCLUDE_DELETED) includeDeleted: Boolean, @Query(EVENT) eventUid: String? = null, - ): NTIPayload + ): TrackerPayload @GET(EVENTS) suspend fun getEventSingle( @Query(FIELDS) @Which fields: Fields, @Query(EVENT) eventUid: String, @Query(OU_MODE) orgUnitMode: String, - ): NTIPayload + ): TrackerPayload companion object { const val TRACKED_ENTITY_INSTANCES = "tracker/trackedEntities" diff --git a/core/src/sharedTest/resources/event/new_tracker_importer_events_greater_equal_v41.json b/core/src/sharedTest/resources/event/new_tracker_importer_events_greater_equal_v41.json new file mode 100644 index 0000000000..1d4031b4fe --- /dev/null +++ b/core/src/sharedTest/resources/event/new_tracker_importer_events_greater_equal_v41.json @@ -0,0 +1,35 @@ +{ + "pager": { + "page": 1, + "pageSize": 50 + }, + "page": 1, + "pageSize": 50, + "events": [ + { + "attributeOptionCombo": "bRowv6yZOF2", + "programStage": "dBwrot7S420", + "orgUnit": "DiszpKrYNg8", + "scheduledAt": "2017-01-28T00:00:00.000", + "program": "lxAQ7Zs9VYR", + "event": "single1", + "status": "COMPLETED", + "occurredAt": "2018-02-27T00:00:00.000", + "dataValues": [ + ] + }, + { + "attributeOptionCombo": "bRowv6yZOF2", + "programStage": "dBwrot7S420", + "orgUnit": "DiszpKrYNg8", + "scheduledAt": "2018-02-28T00:00:00.000", + "trackedEntityInstance": "nWrB0TfWlvh", + "program": "lxAQ7Zs9VYR", + "event": "single2", + "status": "ACTIVE", + "occurredAt": "2017-02-27T00:00:00.000", + "dataValues": [ + ] + } + ] +} \ No newline at end of file diff --git a/core/src/sharedTest/resources/event/new_tracker_importer_events_lower_v41.json b/core/src/sharedTest/resources/event/new_tracker_importer_events_lower_v41.json new file mode 100644 index 0000000000..1ee0269c8f --- /dev/null +++ b/core/src/sharedTest/resources/event/new_tracker_importer_events_lower_v41.json @@ -0,0 +1,31 @@ +{ + "page": 1, + "pageSize": 50, + "instances": [ + { + "attributeOptionCombo": "bRowv6yZOF2", + "programStage": "dBwrot7S420", + "orgUnit": "DiszpKrYNg8", + "scheduledAt": "2017-01-28T00:00:00.000", + "program": "lxAQ7Zs9VYR", + "event": "single1", + "status": "COMPLETED", + "occurredAt": "2018-02-27T00:00:00.000", + "dataValues": [ + ] + }, + { + "attributeOptionCombo": "bRowv6yZOF2", + "programStage": "dBwrot7S420", + "orgUnit": "DiszpKrYNg8", + "scheduledAt": "2018-02-28T00:00:00.000", + "trackedEntityInstance": "nWrB0TfWlvh", + "program": "lxAQ7Zs9VYR", + "event": "single2", + "status": "ACTIVE", + "occurredAt": "2017-02-27T00:00:00.000", + "dataValues": [ + ] + } + ] +} \ No newline at end of file diff --git a/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_greater_equal_v41.json b/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_greater_equal_v41.json new file mode 100644 index 0000000000..9d2d415a22 --- /dev/null +++ b/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_greater_equal_v41.json @@ -0,0 +1,34 @@ +{ + "pager": { + "page": 1, + "pageSize": 50 + }, + "page": 1, + "pageSize": 50, + "trackedEntities": [ + { + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "trackedEntity": "nWrB0TfWlvh", + "deleted": false, + "attributes": [ + { + "attribute": "cejWyOfXge6", + "value": "4081507" + } + ] + }, + { + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "trackedEntity": "nWrB0TfWlvD", + "deleted": false, + "attributes": [ + { + "attribute": "cejWyOfXge6", + "value": "654321" + } + ] + } + ] +} \ No newline at end of file diff --git a/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_lower_v41.json b/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_lower_v41.json new file mode 100644 index 0000000000..6c64651c1e --- /dev/null +++ b/core/src/sharedTest/resources/trackedentity/new_tracker_importer_tracked_entities_lower_v41.json @@ -0,0 +1,30 @@ +{ + "page": 1, + "pageSize": 50, + "instances": [ + { + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "trackedEntity": "nWrB0TfWlvh", + "deleted": false, + "attributes": [ + { + "attribute": "cejWyOfXge6", + "value": "4081507" + } + ] + }, + { + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "trackedEntity": "nWrB0TfWlvD", + "deleted": false, + "attributes": [ + { + "attribute": "cejWyOfXge6", + "value": "654321" + } + ] + } + ] +} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadGreaterEqualV41Should.kt b/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadGreaterEqualV41Should.kt new file mode 100644 index 0000000000..c37482eca7 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadGreaterEqualV41Should.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.event + +import com.fasterxml.jackson.core.type.TypeReference +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class NewTrackerImporterEventPayloadGreaterEqualV41Should : + BaseObjectShould("event/new_tracker_importer_events_greater_equal_v41.json"), + ObjectShould { + + @Test + override fun map_from_json_string() { + val eventPayload = objectMapper.readValue( + jsonStream, + object : TypeReference>() {}, + ) + + assertThat(eventPayload.pager()?.page).isEqualTo(1) + assertThat(eventPayload.pager()?.pageSize).isEqualTo(50) + assertThat(eventPayload.items().size).isEqualTo(2) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadLowerV41Should.kt b/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadLowerV41Should.kt new file mode 100644 index 0000000000..7011904766 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/event/NewTrackerImporterEventPayloadLowerV41Should.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.event + +import com.fasterxml.jackson.core.type.TypeReference +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class NewTrackerImporterEventPayloadLowerV41Should : + BaseObjectShould("event/new_tracker_importer_events_lower_v41.json"), + ObjectShould { + + @Test + override fun map_from_json_string() { + val eventPayload = objectMapper.readValue( + jsonStream, + object : TypeReference>() {}, + ) + + assertThat(eventPayload.pager()?.page).isEqualTo(1) + assertThat(eventPayload.pager()?.pageSize).isEqualTo(50) + assertThat(eventPayload.items().size).isEqualTo(2) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadGreaterEqualV41Should.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadGreaterEqualV41Should.kt new file mode 100644 index 0000000000..e42163727f --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadGreaterEqualV41Should.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.trackedentity + +import com.fasterxml.jackson.core.type.TypeReference +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class NewTrackerImporterTrackedEntityPayloadGreaterEqualV41Should : + BaseObjectShould("trackedentity/new_tracker_importer_tracked_entities_greater_equal_v41.json"), + ObjectShould { + + @Test + override fun map_from_json_string() { + val trackedEntityPayload = objectMapper.readValue( + jsonStream, + object : TypeReference>() {}, + ) + + assertThat(trackedEntityPayload.pager()?.page).isEqualTo(1) + assertThat(trackedEntityPayload.pager()?.pageSize).isEqualTo(50) + assertThat(trackedEntityPayload.items().size).isEqualTo(2) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadLowerV41Should.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadLowerV41Should.kt new file mode 100644 index 0000000000..1c8731b2ed --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/NewTrackerImporterTrackedEntityPayloadLowerV41Should.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.trackedentity + +import com.fasterxml.jackson.core.type.TypeReference +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.api.payload.internal.TrackerPayload +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class NewTrackerImporterTrackedEntityPayloadLowerV41Should : + BaseObjectShould("trackedentity/new_tracker_importer_tracked_entities_lower_v41.json"), + ObjectShould { + + @Test + override fun map_from_json_string() { + val trackedEntityPayload = objectMapper.readValue( + jsonStream, + object : TypeReference>() {}, + ) + + assertThat(trackedEntityPayload.pager()?.page).isEqualTo(1) + assertThat(trackedEntityPayload.pager()?.pageSize).isEqualTo(50) + assertThat(trackedEntityPayload.items().size).isEqualTo(2) + } +} From 6bf0d1faa03dae41f2dca97c3abe02b710bdde04 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 26 Jan 2024 10:05:37 +0100 Subject: [PATCH 039/222] [PR-ANDROSDK-1789] Do not page backwards in Tracker pagingSources --- .../TrackedEntityInstanceQueryPagingSource.kt | 33 +++++++++++++++---- .../search/TrackedEntitySearchDataSource.kt | 2 +- .../search/TrackedEntitySearchPagingSource.kt | 33 +++++++++++++++---- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt index df6cb3bb4e..9a8f1e91d7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryPagingSource.kt @@ -59,10 +59,6 @@ internal class TrackedEntityInstanceQueryPagingSource( localQueryHelper, ) - init { - dataFetcher.refresh() - } - override fun getRefreshKey( state: PagingState, ): TrackedEntityInstance? { @@ -72,14 +68,37 @@ internal class TrackedEntityInstanceQueryPagingSource( override suspend fun load( params: LoadParams, ): LoadResult { - val pages = dataFetcher.loadPages(params.loadSize) + return when (params) { + is LoadParams.Refresh -> { + dataFetcher.refresh() + loadPages(params.loadSize) + } + is LoadParams.Append -> { + loadPages(params.loadSize) + } + is LoadParams.Prepend -> { + emptyPage() + } + } + } + + private fun loadPages(loadSize: Int): LoadResult { + val pages = dataFetcher.loadPages(loadSize) return pages.firstOrNull { it is Result.Failure }?.let { LoadResult.Error((it as Result.Failure).failure) } ?: LoadResult.Page( data = pages.map { it.getOrThrow() }, - prevKey = pages.firstOrNull()?.getOrThrow(), - nextKey = pages.getOrNull(params.loadSize - 1)?.getOrThrow(), + prevKey = null, // Only paging forward + nextKey = pages.getOrNull(loadSize - 1)?.getOrThrow(), + ) + } + + private fun emptyPage(): LoadResult { + return LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null, ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchDataSource.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchDataSource.kt index ea8183c5a7..52514aaea9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchDataSource.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchDataSource.kt @@ -30,7 +30,7 @@ package org.hisp.dhis.android.core.trackedentity.search import androidx.paging.ItemKeyedDataSource import org.hisp.dhis.android.core.arch.helpers.Result -internal class TrackedEntitySearchDataSource constructor( +internal class TrackedEntitySearchDataSource( private val dataFetcher: TrackedEntitySearchDataFetcher, ) : ItemKeyedDataSource() { diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt index 69c35de1ae..f489e0b9f3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntitySearchPagingSource.kt @@ -61,10 +61,6 @@ internal class TrackedEntitySearchPagingSource( helper, ) - init { - dataFetcher.refresh() - } - override fun getRefreshKey( state: PagingState, ): TrackedEntitySearchItem? { @@ -74,14 +70,37 @@ internal class TrackedEntitySearchPagingSource( override suspend fun load( params: LoadParams, ): LoadResult { - val pages = dataFetcher.loadPages(params.loadSize) + return when (params) { + is LoadParams.Refresh -> { + dataFetcher.refresh() + loadPages(params.loadSize) + } + is LoadParams.Append -> { + loadPages(params.loadSize) + } + is LoadParams.Prepend -> { + emptyPage() + } + } + } + + private fun loadPages(loadSize: Int): LoadResult { + val pages = dataFetcher.loadPages(loadSize) return pages.firstOrNull { it is Result.Failure }?.let { LoadResult.Error((it as Result.Failure).failure) } ?: LoadResult.Page( data = pages.map { it.getOrThrow() }, - prevKey = pages.firstOrNull()?.getOrThrow(), - nextKey = pages.getOrNull(params.loadSize - 1)?.getOrThrow(), + prevKey = null, // Only paging forward + nextKey = pages.getOrNull(loadSize - 1)?.getOrThrow(), + ) + } + + private fun emptyPage(): LoadResult { + return LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null, ) } } From 60d7d5ffa30836c195cd10cdd1e6f3e71dc85c45 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 25 Jan 2024 11:21:14 +0100 Subject: [PATCH 040/222] [PR-ANDROSDK-1789] Revert temp version number --- Jenkinsfile | 2 -- core/gradle.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5a48328cfb..fe0e0853db 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -74,8 +74,6 @@ pipeline { expression { env.GIT_BRANCH == "master" } expression { env.GIT_BRANCH == "develop" } expression { env.GIT_BRANCH ==~ /[0-9]+\.[0-9]+\.[0-9]+-rc/ } - - expression { env.GIT_BRANCH == "PR-ANDROSDK-1789" } } } } diff --git a/core/gradle.properties b/core/gradle.properties index eba77ea4df..212f0f2af5 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -29,7 +29,7 @@ # Properties which are consumed by plugins/gradle-mvn-push.gradle plugin. # They are used for publishing artifact to snapshot repository. -VERSION_NAME=1.9.1-1789-SNAPSHOT +VERSION_NAME=1.10.0-SNAPSHOT VERSION_CODE=292 GROUP=org.hisp.dhis From af8b6931c3c7132a19c0a886a070bda7ff3c0a4f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 30 Jan 2024 13:04:35 +0100 Subject: [PATCH 041/222] [ANDROSDK-1808] TrackerLineList model --- .../trackerlinelist/TrackerLineListModel.kt | 92 +++++++++++++++++++ .../TrackerLineListRepository.kt | 51 ++++++++++ .../TrackerLineListResponse.kt | 43 +++++++++ 3 files changed, 186 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt new file mode 100644 index 0000000000..b4d72e042a --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import org.hisp.dhis.android.core.common.RelativeOrganisationUnit +import org.hisp.dhis.android.core.common.RelativePeriod +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus + +sealed class TrackerLineListItem(val id: String) { + + sealed class OrganisationUnitItem(id: String) : TrackerLineListItem(id) { + data class Absolute(val uid: String) : OrganisationUnitItem(uid) + data class Relative(val relative: RelativeOrganisationUnit) : OrganisationUnitItem(relative.name) + data class Level(val uid: String) : OrganisationUnitItem(uid) + data class Group(val uid: String) : OrganisationUnitItem(uid) + } + + sealed class DateItem(id: String) : TrackerLineListItem(id) { + data class LastUpdated(val filters: List) : DateItem(Label.LastUpdated) + data class IncidentDate(val filters: List) : DateItem(Label.IncidentDate) + data class EnrollmentDate(val filters: List) : DateItem(Label.EnrollmentDate) + data class ScheduledDate(val filters: List) : DateItem(Label.ScheduledDate) + data class EventDate(val filters: List) : DateItem(Label.EventDate) + } + + data class ProgramIndicator(val uid: String, val filters: List) : TrackerLineListItem(uid) + + data class ProgramAttribute(val uid: String, val filters: List) : TrackerLineListItem(uid) + + data class ProgramDataElement( + val uid: String, + val program: String, + val programStage: String, + val filters: List + ) : TrackerLineListItem("$program.$programStage.$uid") + + object CreatedBy: TrackerLineListItem(Label.CreatedBy) + + object LastUpdatedBy: TrackerLineListItem(Label.LastUpdatedBy) + + data class ProgramStatus(val filters: List): TrackerLineListItem(Label.ProgramStatus) + + data class EventStatus(val filters: List): TrackerLineListItem(Label.EventStatus) +} + +sealed class DateFilter() { + data class Relative(val relative: RelativePeriod) : DateFilter() + data class Absolute(val uid: String) : DateFilter() + data class Range(val startDate: String, val endDate: String) : DateFilter() +} + +sealed class DataFilter() { + data class GreaterThan(val value: String) : DataFilter() +} + +internal object Label { + const val LastUpdated = "lastUpdated" + const val IncidentDate = "incidentDate" + const val EnrollmentDate = "enrollmentDate" + const val ScheduledDate = "scheduledDate" + const val EventDate = "eventDate" + const val CreatedBy = "createdBy" + const val LastUpdatedBy = "lastUpdatedBy" + const val ProgramStatus = "programStatus" + const val EventStatus = "eventStatus" +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt new file mode 100644 index 0000000000..99c554560d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import io.reactivex.Single +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.arch.helpers.Result + +interface TrackerLineListRepository { + + fun withEventOutput(programId: String, programStageId: String): TrackerLineListRepository + + fun withEnrollmentOutput(programId: String): TrackerLineListRepository + + fun withColumn(column: TrackerLineListItem): TrackerLineListRepository + + fun withFilter(filter: TrackerLineListItem): TrackerLineListRepository + + // TODO + fun withTrackerVisualization(): TrackerLineListRepository + + fun evaluate(): Single> + + fun blockingEvaluate(): Result +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt new file mode 100644 index 0000000000..f6338d4971 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem + +data class TrackerLineListResponse( + val metadata: Map, + val headers: List, + val filters: List, + val rows: List, +) + +data class TrackerLineListValue( + val metadataItem: String, + val value: String?, +) From ad862dea53a0099ad7e554c189fcd1578310be83 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 2 Feb 2024 08:32:17 +0100 Subject: [PATCH 042/222] [ANDROSDK-1808] TrackerLineList service (skeleton) --- ...ckerLineListRepositoryIntegrationShould.kt | 50 ++++ .../core/analytics/AnalyticsDIModule.kt | 6 + .../android/core/analytics/AnalyticsModule.kt | 3 + .../core/analytics/AnalyticsModuleImpl.kt | 4 + .../analytics/aggregated/AnalyticsModel.kt | 2 + .../trackerlinelist/TrackerLineListModel.kt | 12 + .../TrackerLineListRepository.kt | 2 +- .../internal/DataFilterHelper.kt | 61 +++++ .../internal/TrackerLineListOutputType.kt | 34 +++ .../internal/TrackerLineListParams.kt | 39 +++ .../internal/TrackerLineListRepositoryImpl.kt | 95 +++++++ .../internal/TrackerLineListService.kt | 118 +++++++++ .../TrackerLineListServiceMetadataHelper.kt | 234 ++++++++++++++++++ .../evaluator/ProgramAttributeEvaluator.kt | 62 +++++ .../evaluator/TrackerLineListEvaluator.kt | 36 +++ .../TrackerLineListEvaluatorMapper.kt | 41 +++ .../evaluator/TrackerLineListSQLLabel.kt | 34 +++ 17 files changed, 832 insertions(+), 1 deletion(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListOutputType.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt new file mode 100644 index 0000000000..6fd4d34ef3 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.junit.Test + +class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDispatcher() { + @Test + fun evaluate_program_attributes() { + val result = d2.analyticsModule().trackerLineList() + .withEventOutput("IpHINAT79UW", "dBwrot7S420") + .withColumn( + TrackerLineListItem.ProgramAttribute( + uid = "cejWyOfXge6", + filters = listOf(DataFilter.GreaterThan("789")) + ) + ) + .blockingEvaluate() + + assertThat(result.getOrThrow().rows.size).isEqualTo(2) + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt index cbaaa1ed55..2b35eb85c3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.analytics import org.hisp.dhis.android.core.analytics.aggregated.internal.AnalyticsRepositoryParams import org.hisp.dhis.android.core.analytics.aggregated.internal.AnalyticsVisualizationsRepositoryParams import org.hisp.dhis.android.core.analytics.linelist.EventLineListParams +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListParams import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module import org.koin.core.annotation.Singleton @@ -52,4 +53,9 @@ internal class AnalyticsDIModule { fun emptyAnalyticsVisualizationsParam(): AnalyticsVisualizationsRepositoryParams { return AnalyticsVisualizationsRepositoryParams(null, null, null) } + + @Singleton + fun emptyTrackerLineListParams(): TrackerLineListParams { + return TrackerLineListParams(null, null, null, emptyList(), emptyList()) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModule.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModule.kt index d9a32380fc..4caa013929 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModule.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.analytics import org.hisp.dhis.android.core.analytics.aggregated.AnalyticsRepository import org.hisp.dhis.android.core.analytics.aggregated.AnalyticsVisualizationsRepository import org.hisp.dhis.android.core.analytics.linelist.EventLineListRepository +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository interface AnalyticsModule { @@ -38,4 +39,6 @@ interface AnalyticsModule { fun analytics(): AnalyticsRepository fun visualizations(): AnalyticsVisualizationsRepository + + fun trackerLineList(): TrackerLineListRepository } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModuleImpl.kt index 4f78431fc2..10b8c07672 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsModuleImpl.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.analytics import org.hisp.dhis.android.core.analytics.aggregated.AnalyticsRepository import org.hisp.dhis.android.core.analytics.aggregated.AnalyticsVisualizationsRepository import org.hisp.dhis.android.core.analytics.linelist.EventLineListRepository +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository import org.koin.core.annotation.Singleton @Singleton @@ -37,6 +38,7 @@ internal class AnalyticsModuleImpl( private val eventLineListRepository: EventLineListRepository, private val analyticsRepository: AnalyticsRepository, private val analyticsVisualizationsRepository: AnalyticsVisualizationsRepository, + private val trackerLineListRepository: TrackerLineListRepository, ) : AnalyticsModule { override fun eventLineList(): EventLineListRepository = eventLineListRepository @@ -44,4 +46,6 @@ internal class AnalyticsModuleImpl( override fun analytics(): AnalyticsRepository = analyticsRepository override fun visualizations(): AnalyticsVisualizationsRepository = analyticsVisualizationsRepository + + override fun trackerLineList(): TrackerLineListRepository = trackerLineListRepository } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt index e625db63c4..18174a6864 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt @@ -50,6 +50,8 @@ sealed class MetadataItem(val id: String, val displayName: String) { class DataElementOperandItem(val item: DataElementOperand, dataElementName: String, cocName: String?) : MetadataItem(item.uid()!!, "$dataElementName $cocName") + class TrackedEntityAttributeItem(val item: TrackedEntityAttribute) : MetadataItem(item.uid(), item.displayName()!!) + class IndicatorItem(val item: Indicator) : MetadataItem(item.uid(), item.displayName()!!) class ProgramIndicatorItem(val item: ProgramIndicator) : MetadataItem(item.uid(), item.displayName()!!) class EventDataElementItem(val item: DataElement, val program: Program) : diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index b4d72e042a..14958b3bfe 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -76,7 +76,19 @@ sealed class DateFilter() { } sealed class DataFilter() { + data class EqualTo(val value: String): DataFilter() + data class NotEqualTo(val value: String): DataFilter() + data class EqualToIgnoreCase(val value: String): DataFilter() + data class NotEqualToIgnoreCase(val value: String): DataFilter() data class GreaterThan(val value: String) : DataFilter() + data class GreaterThanOrEqualTo(val value: String) : DataFilter() + data class LessThan(val value: String) : DataFilter() + data class LessThanOrEqualTo(val value: String) : DataFilter() + data class Like(val value: String) : DataFilter() + data class NotLike(val value: String) : DataFilter() + data class LikeIgnoreCase(val value: String) : DataFilter() + data class NotLikeIgnoreCase(val value: String) : DataFilter() + data class In(val values: List) : DataFilter() } internal object Label { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt index 99c554560d..8001bca19e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -34,7 +34,7 @@ import org.hisp.dhis.android.core.arch.helpers.Result interface TrackerLineListRepository { - fun withEventOutput(programId: String, programStageId: String): TrackerLineListRepository + fun withEventOutput(programId: String, programStageId: String?): TrackerLineListRepository fun withEnrollmentOutput(programId: String): TrackerLineListRepository diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt new file mode 100644 index 0000000000..4dcd7252c1 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter + +internal object DataFilterHelper { + fun getWhereClause(itemId: String, filters: List): String { + return if (filters.isEmpty()) { + "1" + } else { + return filters.joinToString(" AND ") { getSqlOperator(itemId, it) } + } + } + + private fun getSqlOperator(itemId: String, filter: DataFilter): String { + val comparison = when (filter) { + is DataFilter.EqualTo -> "= '${filter.value}'" + is DataFilter.NotEqualTo -> "!= '${filter.value}'" + is DataFilter.EqualToIgnoreCase -> "= '${filter.value}' COLLATE NOCASE" + is DataFilter.NotEqualToIgnoreCase -> "!= '${filter.value}' COLLATE NOCASE" + is DataFilter.GreaterThan -> "> ${filter.value}" + is DataFilter.GreaterThanOrEqualTo -> ">= '${filter.value}'" + is DataFilter.LessThan -> "< '${filter.value}'" + is DataFilter.LessThanOrEqualTo -> "<= '${filter.value}'" + is DataFilter.Like -> "= '%${filter.value}%'" + is DataFilter.LikeIgnoreCase -> "= '%${filter.value}%' COLLATE NOCASE" + is DataFilter.NotLike -> "!= '%${filter.value}%'" + is DataFilter.NotLikeIgnoreCase -> "!= '%${filter.value}%' COLLATE NOCASE" + is DataFilter.In -> "IN (${filter.values.joinToString(", ") {"'$it'"}})" + } + + return "\"$itemId\" $comparison" + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListOutputType.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListOutputType.kt new file mode 100644 index 0000000000..001fbff602 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListOutputType.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +internal enum class TrackerLineListOutputType { + EVENT, + ENROLLMENT, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt new file mode 100644 index 0000000000..ae377ff257 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem + +internal data class TrackerLineListParams( + val outputType: TrackerLineListOutputType?, + val programId: String?, + val programStageId: String?, + val columns: List, + val filters: List, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt new file mode 100644 index 0000000000..2a87397ef3 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import io.reactivex.Single +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse +import org.hisp.dhis.android.core.arch.helpers.Result +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerLineListRepositoryImpl( + private val params: TrackerLineListParams, + private val service: TrackerLineListService, +) : TrackerLineListRepository { + + override fun withEventOutput(programId: String, programStageId: String?): TrackerLineListRepositoryImpl { + return updateParams { + params.copy( + outputType = TrackerLineListOutputType.EVENT, + programId = programId, + programStageId = programStageId + ) + } + } + + override fun withEnrollmentOutput(programId: String): TrackerLineListRepositoryImpl { + return updateParams { + params.copy( + outputType = TrackerLineListOutputType.ENROLLMENT, + programId = programId + ) + } + } + + override fun withColumn(column: TrackerLineListItem): TrackerLineListRepositoryImpl { + return updateParams { params.copy(columns = updateItems(params.columns, column)) } + } + + override fun withFilter(filter: TrackerLineListItem): TrackerLineListRepositoryImpl { + return updateParams { params.copy(columns = updateItems(params.filters, filter)) } + } + + // TODO + override fun withTrackerVisualization(): TrackerLineListRepositoryImpl { + return TODO() + } + + override fun evaluate(): Single> { + return Single.fromCallable { blockingEvaluate() } + } + + override fun blockingEvaluate(): Result { + return service.evaluate(params) + } + + private fun updateParams( + func: (params: TrackerLineListParams) -> TrackerLineListParams + ): TrackerLineListRepositoryImpl { + return TrackerLineListRepositoryImpl(func(params), service) + } + + private fun updateItems(items: List, newItem: TrackerLineListItem): List { + val otherItems = items.filterNot { it.id == newItem.id } + return otherItems + newItem + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt new file mode 100644 index 0000000000..36aea91ba7 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo +import org.hisp.dhis.android.core.event.EventTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerLineListService( + private val databaseAdapter: DatabaseAdapter, + private val metadataHelper: TrackerLineListServiceMetadataHelper, +) { + fun evaluate(params: TrackerLineListParams): Result { + // TODO Validate params + + // TODO Build metadata + val metadata = metadataHelper.getMetadata(params) + + val sqlClause = when (params.outputType!!) { + TrackerLineListOutputType.EVENT -> getEventSqlClause(params, metadata) + TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(params) + } + + val cursor = databaseAdapter.rawQuery(sqlClause) + val values = mapCursorToColumns(params, cursor) + + return Result.Success( + TrackerLineListResponse( + metadata = metadata, + headers = emptyList(), + filters = emptyList(), + rows = values + ) + ) + } + + private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { + return "SELECT " + + "${getEventSelectColumns(params, metadata)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, metadata)} " + } + + private fun getEnrollmentSqlClause(params: TrackerLineListParams): String { + return TODO() + } + + private fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List { + val values: MutableList = mutableListOf() + cursor.use { c -> + if (c.count > 0) { + c.moveToFirst() + do { + params.columns.forEachIndexed { index, item -> + values.add(TrackerLineListValue(item.id, cursor.getString(index))) + } + } while (c.moveToNext()) + } + } + return values + } + + private fun getEventSelectColumns(params: TrackerLineListParams, metadata: Map): String { + return params.columns.joinToString(", ") { + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEvent()}) ${it.id}" + } + } + + private fun getEventWhereClause(params: TrackerLineListParams, metadata: Map): String { + return (params.columns + params.filters).joinToString(" AND ") { + TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEvent() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt new file mode 100644 index 0000000000..dc254cd0f0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.aggregated.DimensionItem +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.aggregated.internal.AnalyticsOrganisationUnitHelper +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.category.internal.CategoryOptionComboStore +import org.hisp.dhis.android.core.category.internal.CategoryOptionStore +import org.hisp.dhis.android.core.category.internal.CategoryStore +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.dataelement.DataElementOperand +import org.hisp.dhis.android.core.dataelement.internal.DataElementStore +import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemStore +import org.hisp.dhis.android.core.indicator.internal.IndicatorStore +import org.hisp.dhis.android.core.legendset.internal.LegendStore +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitGroupStore +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelStore +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitStore +import org.hisp.dhis.android.core.period.internal.ParentPeriodGenerator +import org.hisp.dhis.android.core.period.internal.PeriodHelper +import org.hisp.dhis.android.core.program.ProgramIndicatorCollectionRepository +import org.hisp.dhis.android.core.program.internal.ProgramStore +import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStore +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("LongParameterList") +internal class TrackerLineListServiceMetadataHelper( + private val categoryStore: CategoryStore, + private val categoryOptionStore: CategoryOptionStore, + private val categoryOptionComboStore: CategoryOptionComboStore, + private val dataElementStore: DataElementStore, + private val indicatorStore: IndicatorStore, + private val expressionDimensionItemStore: ExpressionDimensionItemStore, + private val legendStore: LegendStore, + private val organisationUnitStore: OrganisationUnitStore, + private val organisationUnitGroupStore: OrganisationUnitGroupStore, + private val organisationUnitLevelStore: OrganisationUnitLevelStore, + private val programStore: ProgramStore, + private val trackedEntityAttributeStore: TrackedEntityAttributeStore, + private val programIndicatorRepository: ProgramIndicatorCollectionRepository, + private val analyticsOrganisationUnitHelper: AnalyticsOrganisationUnitHelper, + private val parentPeriodGenerator: ParentPeriodGenerator, + private val periodHelper: PeriodHelper, +) { + + fun getMetadata(params: TrackerLineListParams): Map { + val metadata: MutableMap = mutableMapOf() + + (params.columns + params.filters).forEach { item -> + metadata += getMetadata(item) + } + + return metadata + } + + private fun getMetadata(item: TrackerLineListItem): Map { + val metadata: MutableMap = mutableMapOf() + + if (!metadata.containsKey(item.id)) { + val metadataItems = when (item) { + is TrackerLineListItem.ProgramAttribute -> getProgramAttributeItems(item) + else -> emptyList() + } + val metadataItemsMap = metadataItems.associateBy { it.id } + + metadata += metadataItemsMap + } + + return metadata + } + + private fun getProgramAttributeItems(item: TrackerLineListItem.ProgramAttribute): List { + val attribute = trackedEntityAttributeStore.selectByUid(item.uid) + ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.uid) + + return listOf( + MetadataItem.TrackedEntityAttributeItem(attribute) + ) + } + + @SuppressWarnings("ThrowsCount", "ComplexMethod") + private fun getDataItems(item: DimensionItem.DataItem): List { + return listOf( + when (item) { + is DimensionItem.DataItem.DataElementItem -> + dataElementStore.selectByUid(item.uid) + ?.let { dataElement -> MetadataItem.DataElementItem(dataElement) } + ?: throw AnalyticsException.InvalidDataElement(item.uid) + + is DimensionItem.DataItem.DataElementOperandItem -> { + val dataElement = dataElementStore.selectByUid(item.dataElement) + val coc = categoryOptionComboStore.selectByUid(item.categoryOptionCombo) + if (dataElement == null || coc == null) { + throw AnalyticsException.InvalidDataElementOperand(item.id) + } + val dataElementOperand = DataElementOperand.builder() + .uid("${item.dataElement}.${item.categoryOptionCombo}") + .dataElement(ObjectWithUid.create(item.dataElement)) + .categoryOptionCombo(ObjectWithUid.create(item.categoryOptionCombo)) + .build() + + MetadataItem.DataElementOperandItem( + dataElementOperand, + dataElement.displayName()!!, + coc.displayName(), + ) + } + + is DimensionItem.DataItem.IndicatorItem -> + indicatorStore.selectByUid(item.uid) + ?.let { indicator -> MetadataItem.IndicatorItem(indicator) } + ?: throw AnalyticsException.InvalidIndicator(item.uid) + + is DimensionItem.DataItem.ProgramIndicatorItem -> + programIndicatorRepository.withAnalyticsPeriodBoundaries().uid(item.uid).blockingGet() + ?.let { programIndicator -> MetadataItem.ProgramIndicatorItem(programIndicator) } + ?: throw AnalyticsException.InvalidProgramIndicator(item.uid) + + is DimensionItem.DataItem.EventDataItem.DataElement -> { + val dataElement = dataElementStore.selectByUid(item.dataElement) + ?: throw AnalyticsException.InvalidDataElement(item.id) + val program = programStore.selectByUid(item.program) + ?: throw AnalyticsException.InvalidProgram(item.id) + + MetadataItem.EventDataElementItem(dataElement, program) + } + + is DimensionItem.DataItem.EventDataItem.Attribute -> { + val attribute = trackedEntityAttributeStore.selectByUid(item.attribute) + ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.id) + val program = programStore.selectByUid(item.program) + ?: throw AnalyticsException.InvalidProgram(item.id) + + MetadataItem.EventAttributeItem(attribute, program) + } + + is DimensionItem.DataItem.ExpressionDimensionItem -> { + val expressionItem = expressionDimensionItemStore.selectByUid(item.uid) + ?: throw AnalyticsException.InvalidExpressionDimensionItem(item.uid) + + MetadataItem.ExpressionDimensionItemItem(expressionItem) + } + }, + ) + } + + private fun getPeriodItems(item: DimensionItem.PeriodItem): List { + return listOf( + when (item) { + is DimensionItem.PeriodItem.Absolute -> { + val period = periodHelper.blockingGetPeriodForPeriodId(item.periodId) + MetadataItem.PeriodItem(period) + } + + is DimensionItem.PeriodItem.Relative -> { + val periods = parentPeriodGenerator.generateRelativePeriods(item.relative) + MetadataItem.RelativePeriodItem(item.relative, periods) + } + }, + ) + } + + @SuppressWarnings("ThrowsCount") + private fun getOrganisationUnitItems(item: DimensionItem.OrganisationUnitItem): List { + return listOf( + when (item) { + is DimensionItem.OrganisationUnitItem.Absolute -> + organisationUnitStore.selectByUid(item.uid) + ?.let { organisationUnit -> MetadataItem.OrganisationUnitItem(organisationUnit) } + ?: throw AnalyticsException.InvalidOrganisationUnit(item.uid) + + is DimensionItem.OrganisationUnitItem.Relative -> { + val ouUids = analyticsOrganisationUnitHelper.getRelativeOrganisationUnitUids(item.relative) + MetadataItem.OrganisationUnitRelativeItem(item.relative, ouUids) + } + + is DimensionItem.OrganisationUnitItem.Level -> { + organisationUnitLevelStore.selectByUid(item.uid)?.let { level -> + val ouUids = analyticsOrganisationUnitHelper.getOrganisationUnitUidsByLevel(level.level()!!) + MetadataItem.OrganisationUnitLevelItem(level, ouUids) + } ?: throw AnalyticsException.InvalidOrganisationUnitLevel(item.uid) + } + + is DimensionItem.OrganisationUnitItem.Group -> + organisationUnitGroupStore.selectByUid(item.uid)?.let { group -> + val ouUids = analyticsOrganisationUnitHelper.getOrganisationUnitUidsByGroup(item.uid) + MetadataItem.OrganisationUnitGroupItem(group, ouUids) + } ?: throw AnalyticsException.InvalidOrganisationUnitGroup(item.uid) + }, + ) + } + + private fun getCategoryItems(item: DimensionItem.CategoryItem): List { + return listOf( + categoryStore.selectByUid(item.uid) + ?.let { category -> MetadataItem.CategoryItem(category) } + ?: throw AnalyticsException.InvalidCategory(item.uid), + + categoryOptionStore.selectByUid(item.categoryOption) + ?.let { categoryOption -> MetadataItem.CategoryOptionItem(categoryOption) } + ?: throw AnalyticsException.InvalidCategoryOption(item.categoryOption), + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt new file mode 100644 index 0000000000..2d6c3a153d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo + +internal class ProgramAttributeEvaluator( + private val item: TrackerLineListItem.ProgramAttribute, + private val metadata: Map, +) : TrackerLineListEvaluator { + override fun getSelectSQLForEvent(): String { + return "SELECT " + + "${TrackedEntityAttributeValueTableInfo.Columns.VALUE} " + + "FROM ${TrackedEntityAttributeValueTableInfo.TABLE_INFO.name()} " + + "WHERE ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_INSTANCE} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.TRACKED_ENTITY_INSTANCE} " + + "AND ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE} = '${item.id}'" + } + + override fun getWhereSQLForEvent(): String { + return DataFilterHelper.getWhereClause(item.id, item.filters) + } + + override fun getSelectSQLForEnrollment(): String { + TODO("Not yet implemented") + } + + override fun getWhereSQLForEnrollment(): String { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt new file mode 100644 index 0000000000..fafd5397f2 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +internal interface TrackerLineListEvaluator { + fun getSelectSQLForEvent(): String + fun getWhereSQLForEvent(): String + fun getSelectSQLForEnrollment(): String + fun getWhereSQLForEnrollment(): String +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt new file mode 100644 index 0000000000..172eb2cba1 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem + +internal object TrackerLineListEvaluatorMapper { + fun getEvaluator(item: TrackerLineListItem, metadata: Map): TrackerLineListEvaluator { + return when (item) { + is TrackerLineListItem.ProgramAttribute -> ProgramAttributeEvaluator(item, metadata) + else -> TODO() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt new file mode 100644 index 0000000000..23f3f37b3d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +object TrackerLineListSQLLabel { + const val EventAlias = "ev" + const val EnrollmentAlias = "en" +} \ No newline at end of file From da59a048ca8f434eb69907de10fa43bd2601d38a Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 2 Feb 2024 10:05:30 +0100 Subject: [PATCH 043/222] [ANDROSDK-1806] Refactor program related classes to kotlin --- ...esFields.java => AttributeValuesFields.kt} | 34 +++--- ...aAccessFields.java => DataAccessFields.kt} | 30 +++-- .../internal/DataElementFields.java | 84 -------------- .../dataelement/internal/DataElementFields.kt | 77 +++++++++++++ .../core/dataset/internal/DataSetFields.java | 96 ---------------- .../core/dataset/internal/DataSetFields.kt | 87 +++++++++++++++ .../{AccessLevel.java => AccessLevel.kt} | 12 +- .../core/program/internal/ProgramFields.java | 104 ------------------ .../core/program/internal/ProgramFields.kt | 99 +++++++++++++++++ ...elds.java => ProgramRuleVariableFields.kt} | 44 ++++---- ...ionFields.java => ProgramSectionFields.kt} | 63 +++++------ ....java => ProgramStageDataElementFields.kt} | 64 +++++------ .../program/internal/ProgramStageFields.java | 102 ----------------- .../program/internal/ProgramStageFields.kt | 97 ++++++++++++++++ ...elds.java => ProgramStageSectionFields.kt} | 49 ++++----- ...=> ProgramTrackedEntityAttributeFields.kt} | 50 ++++----- ...eFields.java => RelationshipTypeFields.kt} | 72 ++++++------ ...Fields.java => TrackedEntityTypeFields.kt} | 75 ++++++------- .../android/core/program/ProgramShould.kt | 76 +++++++++++++ .../core/program/ProgramStageShould.kt | 94 ++++++++++++++++ 20 files changed, 755 insertions(+), 654 deletions(-) rename core/src/main/java/org/hisp/dhis/android/core/attribute/internal/{AttributeValuesFields.java => AttributeValuesFields.kt} (70%) rename core/src/main/java/org/hisp/dhis/android/core/common/internal/{DataAccessFields.java => DataAccessFields.kt} (74%) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.kt delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.kt rename core/src/main/java/org/hisp/dhis/android/core/program/{AccessLevel.java => AccessLevel.kt} (92%) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt rename core/src/main/java/org/hisp/dhis/android/core/program/internal/{ProgramRuleVariableFields.java => ProgramRuleVariableFields.kt} (64%) rename core/src/main/java/org/hisp/dhis/android/core/program/internal/{ProgramSectionFields.java => ProgramSectionFields.kt} (54%) rename core/src/main/java/org/hisp/dhis/android/core/program/internal/{ProgramStageDataElementFields.java => ProgramStageDataElementFields.kt} (58%) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt rename core/src/main/java/org/hisp/dhis/android/core/program/internal/{ProgramStageSectionFields.java => ProgramStageSectionFields.kt} (60%) rename core/src/main/java/org/hisp/dhis/android/core/program/internal/{ProgramTrackedEntityAttributeFields.java => ProgramTrackedEntityAttributeFields.kt} (60%) rename core/src/main/java/org/hisp/dhis/android/core/relationship/internal/{RelationshipTypeFields.java => RelationshipTypeFields.kt} (52%) rename core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/{TrackedEntityTypeFields.java => TrackedEntityTypeFields.kt} (56%) create mode 100644 core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.java b/core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.kt similarity index 70% rename from core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.java rename to core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.kt index a9b121f6b9..588c26631c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/attribute/internal/AttributeValuesFields.kt @@ -25,26 +25,22 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.attribute.internal -package org.hisp.dhis.android.core.attribute.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.attribute.AttributeValue +import org.hisp.dhis.android.core.common.ObjectWithUid -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.attribute.AttributeValue; -import org.hisp.dhis.android.core.common.ObjectWithUid; +internal object AttributeValuesFields { + const val VALUE = "value" + const val ATTRIBUTE = "attribute" -public final class AttributeValuesFields { - public static final String VALUE = "value"; - public static final String ATTRIBUTE = "attribute"; + private val fh = FieldsHelper() - private static final FieldsHelper fh = new FieldsHelper<>(); - - public static final Fields allFields = Fields.builder() - .fields( - fh.field(VALUE), - fh.nestedField(ATTRIBUTE).with(ObjectWithUid.uid) - ).build(); - - private AttributeValuesFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields( + fh.field(VALUE), + fh.nestedField(ATTRIBUTE).with(ObjectWithUid.uid), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.java b/core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.kt similarity index 74% rename from core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.java rename to core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.kt index af33b21dab..ca5cf71b29 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/common/internal/DataAccessFields.kt @@ -25,26 +25,22 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.common.internal -package org.hisp.dhis.android.core.common.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Field +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.common.DataAccess -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.common.DataAccess; +internal object DataAccessFields { + private const val READ = "read" + private const val WRITE = "write" -public final class DataAccessFields { + val read: Field = Field.create(READ) - private static final String READ = "read"; - private static final String WRITE = "write"; + val write: Field = Field.create(WRITE) - public static final Field read = Field.create(READ); - public static final Field write = Field.create(WRITE); - - public static final Fields allFields = Fields.builder().fields( - read, - write - ).build(); - - private DataAccessFields() { - } + val allFields: Fields = Fields.builder().fields( + read, + write, + ).build() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.java b/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.java deleted file mode 100644 index 3cf682f402..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.dataelement.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.attribute.AttributeValue; -import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.common.ValueType; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.dataelement.DataElement; -import org.hisp.dhis.android.core.dataelement.DataElementTableInfo; -import org.hisp.dhis.android.core.legendset.LegendSet; -import org.hisp.dhis.android.core.legendset.internal.LegendSetFields; - -public final class DataElementFields { - - private final static String STYLE = "style"; - private final static String ACCESS = "access"; - public static final String LEGEND_SETS = "legendSets"; - public static final String ATTRIBUTE_VALUES = "attributeValues"; - - private static final FieldsHelper fh = new FieldsHelper<>(); - - public static final Field uid = fh.uid(); - - static final Field lastUpdated = fh.lastUpdated(); - - public static final Fields allFields = Fields.builder() - .fields(fh.getNameableFields()) - .fields( - fh.field(DataElementTableInfo.Columns.VALUE_TYPE), - fh.field(DataElementTableInfo.Columns.ZERO_IS_SIGNIFICANT), - fh.field(DataElementTableInfo.Columns.AGGREGATION_TYPE), - fh.field(DataElementTableInfo.Columns.FORM_NAME), - fh.field(DataElementTableInfo.Columns.DOMAIN_TYPE), - fh.field(DataElementTableInfo.Columns.DISPLAY_FORM_NAME), - fh.nestedField(DataElementTableInfo.Columns.OPTION_SET) - .with(ObjectWithUid.uid), - fh.nestedField(DataElementTableInfo.Columns.CATEGORY_COMBO) - .with(ObjectWithUid.uid), - fh.field(DataElementTableInfo.Columns.FIELD_MASK), - fh.nestedField(STYLE) - .with(ObjectStyleFields.allFields), - fh.nestedField(ACCESS) - .with(AccessFields.read), - fh.nestedField(LEGEND_SETS).with(LegendSetFields.uid), - fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields) - ).build(); - - private DataElementFields() { - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.kt b/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.kt new file mode 100644 index 0000000000..9bf61f7e1b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/dataelement/internal/DataElementFields.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.dataelement.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.attribute.AttributeValue +import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.dataelement.DataElement +import org.hisp.dhis.android.core.dataelement.DataElementTableInfo +import org.hisp.dhis.android.core.legendset.LegendSet +import org.hisp.dhis.android.core.legendset.internal.LegendSetFields + +internal object DataElementFields { + private const val STYLE = "style" + private const val ACCESS = "access" + const val LEGEND_SETS = "legendSets" + const val ATTRIBUTE_VALUES = "attributeValues" + + private val fh = FieldsHelper() + val uid = fh.uid() + + val lastUpdated = fh.lastUpdated() + + val allFields: Fields = Fields.builder() + .fields(fh.getNameableFields()) + .fields( + fh.field(DataElementTableInfo.Columns.VALUE_TYPE), + fh.field(DataElementTableInfo.Columns.ZERO_IS_SIGNIFICANT), + fh.field(DataElementTableInfo.Columns.AGGREGATION_TYPE), + fh.field(DataElementTableInfo.Columns.FORM_NAME), + fh.field(DataElementTableInfo.Columns.DOMAIN_TYPE), + fh.field(DataElementTableInfo.Columns.DISPLAY_FORM_NAME), + fh.nestedField(DataElementTableInfo.Columns.OPTION_SET) + .with(ObjectWithUid.uid), + fh.nestedField(DataElementTableInfo.Columns.CATEGORY_COMBO) + .with(ObjectWithUid.uid), + fh.field(DataElementTableInfo.Columns.FIELD_MASK), + fh.nestedField(STYLE) + .with(ObjectStyleFields.allFields), + fh.nestedField(ACCESS) + .with(AccessFields.read), + fh.nestedField(LEGEND_SETS).with(LegendSetFields.uid), + fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.java b/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.java deleted file mode 100644 index 584b4f9f61..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.dataset.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.internal.DataAccessFields; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.dataelement.DataElementOperand; -import org.hisp.dhis.android.core.dataelement.internal.DataElementOperandFields; -import org.hisp.dhis.android.core.dataset.DataInputPeriod; -import org.hisp.dhis.android.core.dataset.DataSet; -import org.hisp.dhis.android.core.dataset.DataSetElement; -import org.hisp.dhis.android.core.dataset.Section; -import org.hisp.dhis.android.core.period.PeriodType; - -import static org.hisp.dhis.android.core.dataset.DataSetTableInfo.Columns; - -public final class DataSetFields { - - public static final String DATA_SET_ELEMENTS = "dataSetElements"; - public static final String INDICATORS = "indicators"; - public static final String SECTIONS = "sections"; - public static final String COMPULSORY_DATA_ELEMENT_OPERANDS = "compulsoryDataElementOperands"; - public static final String DATA_INPUT_PERIODS = "dataInputPeriods"; - private static final String ACCESS = "access"; - private static final String STYLE = "style"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - public static final Field uid = fh.uid(); - - static final Fields allFields = Fields.builder() - .fields(fh.getNameableFields()) - .fields( - fh.field(Columns.PERIOD_TYPE), - fh.nestedFieldWithUid(Columns.CATEGORY_COMBO), - fh.field(Columns.MOBILE), - fh.field(Columns.VERSION), - fh.field(Columns.EXPIRY_DAYS), - fh.field(Columns.TIMELY_DAYS), - fh.field(Columns.NOTIFY_COMPLETING_USER), - fh.field(Columns.OPEN_FUTURE_PERIODS), - fh.field(Columns.FIELD_COMBINATION_REQUIRED), - fh.field(Columns.VALID_COMPLETE_ONLY), - fh.field(Columns.NO_VALUE_REQUIRES_COMMENT), - fh.field(Columns.SKIP_OFFLINE), - fh.field(Columns.DATA_ELEMENT_DECORATION), - fh.field(Columns.RENDER_AS_TABS), - fh.field(Columns.RENDER_HORIZONTALLY), - fh.nestedFieldWithUid(Columns.WORKFLOW), - fh.nestedField(DATA_SET_ELEMENTS).with(DataSetElementFields.allFields), - fh.nestedFieldWithUid(INDICATORS), - fh.
nestedField(SECTIONS).with(SectionFields.allFields), - - fh.nestedField(COMPULSORY_DATA_ELEMENT_OPERANDS) - .with(DataElementOperandFields.allFields), - fh.nestedField(DATA_INPUT_PERIODS).with(DataInputPeriodFields.allFields), - fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), - fh.nestedField(STYLE).with(ObjectStyleFields.allFields) - - ).build(); - - private DataSetFields() {} - -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.kt b/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.kt new file mode 100644 index 0000000000..a232191c4d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/internal/DataSetFields.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.dataset.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.internal.DataAccessFields +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.dataelement.DataElementOperand +import org.hisp.dhis.android.core.dataelement.internal.DataElementOperandFields +import org.hisp.dhis.android.core.dataset.DataInputPeriod +import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSetElement +import org.hisp.dhis.android.core.dataset.DataSetTableInfo +import org.hisp.dhis.android.core.dataset.Section +import org.hisp.dhis.android.core.period.PeriodType + +internal object DataSetFields { + const val DATA_SET_ELEMENTS = "dataSetElements" + const val INDICATORS = "indicators" + const val SECTIONS = "sections" + const val COMPULSORY_DATA_ELEMENT_OPERANDS = "compulsoryDataElementOperands" + const val DATA_INPUT_PERIODS = "dataInputPeriods" + private const val ACCESS = "access" + private const val STYLE = "style" + + private val fh = FieldsHelper() + + val uid = fh.uid() + + val allFields: Fields = Fields.builder() + .fields(fh.getNameableFields()) + .fields( + fh.field(DataSetTableInfo.Columns.PERIOD_TYPE), + fh.nestedFieldWithUid(DataSetTableInfo.Columns.CATEGORY_COMBO), + fh.field(DataSetTableInfo.Columns.MOBILE), + fh.field(DataSetTableInfo.Columns.VERSION), + fh.field(DataSetTableInfo.Columns.EXPIRY_DAYS), + fh.field(DataSetTableInfo.Columns.TIMELY_DAYS), + fh.field(DataSetTableInfo.Columns.NOTIFY_COMPLETING_USER), + fh.field(DataSetTableInfo.Columns.OPEN_FUTURE_PERIODS), + fh.field(DataSetTableInfo.Columns.FIELD_COMBINATION_REQUIRED), + fh.field(DataSetTableInfo.Columns.VALID_COMPLETE_ONLY), + fh.field(DataSetTableInfo.Columns.NO_VALUE_REQUIRES_COMMENT), + fh.field(DataSetTableInfo.Columns.SKIP_OFFLINE), + fh.field(DataSetTableInfo.Columns.DATA_ELEMENT_DECORATION), + fh.field(DataSetTableInfo.Columns.RENDER_AS_TABS), + fh.field(DataSetTableInfo.Columns.RENDER_HORIZONTALLY), + fh.nestedFieldWithUid(DataSetTableInfo.Columns.WORKFLOW), + fh.nestedField(DATA_SET_ELEMENTS).with(DataSetElementFields.allFields), + fh.nestedFieldWithUid(INDICATORS), + fh.nestedField
(SECTIONS).with(SectionFields.allFields), + fh.nestedField(COMPULSORY_DATA_ELEMENT_OPERANDS) + .with(DataElementOperandFields.allFields), + fh.nestedField(DATA_INPUT_PERIODS).with(DataInputPeriodFields.allFields), + fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), + fh.nestedField(STYLE).with(ObjectStyleFields.allFields), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.java b/core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.kt similarity index 92% rename from core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.java rename to core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.kt index 9f34313260..1af476f148 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/AccessLevel.kt @@ -25,9 +25,11 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.program -package org.hisp.dhis.android.core.program; - -public enum AccessLevel { - OPEN, AUDITED, PROTECTED, CLOSED -} \ No newline at end of file +enum class AccessLevel { + OPEN, + AUDITED, + PROTECTED, + CLOSED, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.java deleted file mode 100644 index bedd1a6b75..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.program.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.attribute.AttributeValue; -import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.FeatureType; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.internal.DataAccessFields; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.period.PeriodType; -import org.hisp.dhis.android.core.program.AccessLevel; -import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.android.core.program.ProgramRuleVariable; -import org.hisp.dhis.android.core.program.ProgramSection; -import org.hisp.dhis.android.core.program.ProgramTableInfo.Columns; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute; -import org.hisp.dhis.android.core.program.ProgramType; - -public final class ProgramFields { - public static final String PROGRAM_TRACKED_ENTITY_ATTRIBUTES = "programTrackedEntityAttributes"; - private static final String CAPTURE_COORDINATES = "captureCoordinates"; - public static final String PROGRAM_RULE_VARIABLES = "programRuleVariables"; - private static final String ACCESS = "access"; - private static final String STYLE = "style"; - public static final String PROGRAM_SECTIONS = "programSections"; - public static final String ATTRIBUTE_VALUES = "attributeValues"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - public static final Field uid = fh.uid(); - - static final Fields allFields = Fields.builder() - .fields(fh.getNameableFields()) - .fields( - fh.field(Columns.VERSION), - fh.field(Columns.ONLY_ENROLL_ONCE), - fh.field(Columns.ENROLLMENT_DATE_LABEL), - fh.field(Columns.DISPLAY_INCIDENT_DATE), - fh.field(Columns.INCIDENT_DATE_LABEL), - fh.field(Columns.REGISTRATION), - fh.field(Columns.SELECT_ENROLLMENT_DATES_IN_FUTURE), - fh.field(Columns.DATA_ENTRY_METHOD), - fh.field(Columns.IGNORE_OVERDUE_EVENTS), - fh.field(Columns.SELECT_INCIDENT_DATES_IN_FUTURE), - fh.field(CAPTURE_COORDINATES), - fh.field(Columns.USE_FIRST_STAGE_DURING_REGISTRATION), - fh.field(Columns.DISPLAY_FRONT_PAGE_LIST), - fh.field(Columns.PROGRAM_TYPE), - fh.nestedField(PROGRAM_TRACKED_ENTITY_ATTRIBUTES).with( - ProgramTrackedEntityAttributeFields.allFields), - fh.nestedFieldWithUid(Columns.RELATED_PROGRAM), - fh.nestedFieldWithUid(Columns.TRACKED_ENTITY_TYPE), - fh.nestedFieldWithUid(Columns.CATEGORY_COMBO), - fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), - fh.nestedField(PROGRAM_RULE_VARIABLES) - .with(ProgramRuleVariableFields.allFields), - fh.nestedField(STYLE).with(ObjectStyleFields.allFields), - fh.field(Columns.EXPIRY_DAYS), - fh.field(Columns.COMPLETE_EVENTS_EXPIRY_DAYS), - fh.field(Columns.EXPIRY_PERIOD_TYPE), - fh.field(Columns.MIN_ATTRIBUTES_REQUIRED_TO_SEARCH), - fh.field(Columns.MAX_TEI_COUNT_TO_RETURN), - fh.field(Columns.FEATURE_TYPE), - fh.field(Columns.ACCESS_LEVEL), - fh.nestedField(PROGRAM_SECTIONS).with(ProgramSectionFields.allFields), - fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields) - - ).build(); - - private ProgramFields() { - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt new file mode 100644 index 0000000000..afbdd4484f --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.program.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.attribute.AttributeValue +import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.internal.DataAccessFields +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.AccessLevel +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramRuleVariable +import org.hisp.dhis.android.core.program.ProgramSection +import org.hisp.dhis.android.core.program.ProgramTableInfo +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute +import org.hisp.dhis.android.core.program.ProgramType + +internal object ProgramFields { + const val PROGRAM_TRACKED_ENTITY_ATTRIBUTES = "programTrackedEntityAttributes" + private const val CAPTURE_COORDINATES = "captureCoordinates" + const val PROGRAM_RULE_VARIABLES = "programRuleVariables" + private const val ACCESS = "access" + private const val STYLE = "style" + const val PROGRAM_SECTIONS = "programSections" + const val ATTRIBUTE_VALUES = "attributeValues" + + private val fh = FieldsHelper() + + val uid = fh.uid() + + val allFields: Fields = Fields.builder() + .fields(fh.getNameableFields()) + .fields( + fh.field(ProgramTableInfo.Columns.VERSION), + fh.field(ProgramTableInfo.Columns.ONLY_ENROLL_ONCE), + fh.field(ProgramTableInfo.Columns.ENROLLMENT_DATE_LABEL), + fh.field(ProgramTableInfo.Columns.DISPLAY_INCIDENT_DATE), + fh.field(ProgramTableInfo.Columns.INCIDENT_DATE_LABEL), + fh.field(ProgramTableInfo.Columns.REGISTRATION), + fh.field(ProgramTableInfo.Columns.SELECT_ENROLLMENT_DATES_IN_FUTURE), + fh.field(ProgramTableInfo.Columns.DATA_ENTRY_METHOD), + fh.field(ProgramTableInfo.Columns.IGNORE_OVERDUE_EVENTS), + fh.field(ProgramTableInfo.Columns.SELECT_INCIDENT_DATES_IN_FUTURE), + fh.field(CAPTURE_COORDINATES), + fh.field(ProgramTableInfo.Columns.USE_FIRST_STAGE_DURING_REGISTRATION), + fh.field(ProgramTableInfo.Columns.DISPLAY_FRONT_PAGE_LIST), + fh.field(ProgramTableInfo.Columns.PROGRAM_TYPE), + fh.nestedField(PROGRAM_TRACKED_ENTITY_ATTRIBUTES).with( + ProgramTrackedEntityAttributeFields.allFields, + ), + fh.nestedFieldWithUid(ProgramTableInfo.Columns.RELATED_PROGRAM), + fh.nestedFieldWithUid(ProgramTableInfo.Columns.TRACKED_ENTITY_TYPE), + fh.nestedFieldWithUid(ProgramTableInfo.Columns.CATEGORY_COMBO), + fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), + fh.nestedField(PROGRAM_RULE_VARIABLES) + .with(ProgramRuleVariableFields.allFields), + fh.nestedField(STYLE).with(ObjectStyleFields.allFields), + fh.field(ProgramTableInfo.Columns.EXPIRY_DAYS), + fh.field(ProgramTableInfo.Columns.COMPLETE_EVENTS_EXPIRY_DAYS), + fh.field(ProgramTableInfo.Columns.EXPIRY_PERIOD_TYPE), + fh.field(ProgramTableInfo.Columns.MIN_ATTRIBUTES_REQUIRED_TO_SEARCH), + fh.field(ProgramTableInfo.Columns.MAX_TEI_COUNT_TO_RETURN), + fh.field(ProgramTableInfo.Columns.FEATURE_TYPE), + fh.field(ProgramTableInfo.Columns.ACCESS_LEVEL), + fh.nestedField(PROGRAM_SECTIONS).with(ProgramSectionFields.allFields), + fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.kt similarity index 64% rename from core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.java rename to core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.kt index 9a0289aef6..d8e569dbb1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramRuleVariableFields.kt @@ -25,29 +25,27 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.program.internal -package org.hisp.dhis.android.core.program.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.program.ProgramRuleVariable +import org.hisp.dhis.android.core.program.ProgramRuleVariableSourceType +import org.hisp.dhis.android.core.program.ProgramRuleVariableTableInfo -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.program.ProgramRuleVariable; -import org.hisp.dhis.android.core.program.ProgramRuleVariableSourceType; -import org.hisp.dhis.android.core.program.ProgramRuleVariableTableInfo.Columns; +internal object ProgramRuleVariableFields { + private val fh = FieldsHelper() -final class ProgramRuleVariableFields { - - private static FieldsHelper fh = new FieldsHelper<>(); - static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(Columns.USE_CODE_FOR_OPTION_SET), - fh.nestedFieldWithUid(Columns.PROGRAM), - fh.nestedFieldWithUid(Columns.PROGRAM_STAGE), - fh.nestedFieldWithUid(Columns.DATA_ELEMENT), - fh.nestedFieldWithUid(Columns.TRACKED_ENTITY_ATTRIBUTE), - fh.field(Columns.PROGRAM_RULE_VARIABLE_SOURCE_TYPE) - ).build(); - - private ProgramRuleVariableFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(ProgramRuleVariableTableInfo.Columns.USE_CODE_FOR_OPTION_SET), + fh.nestedFieldWithUid(ProgramRuleVariableTableInfo.Columns.PROGRAM), + fh.nestedFieldWithUid(ProgramRuleVariableTableInfo.Columns.PROGRAM_STAGE), + fh.nestedFieldWithUid(ProgramRuleVariableTableInfo.Columns.DATA_ELEMENT), + fh.nestedFieldWithUid(ProgramRuleVariableTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE), + fh.field( + ProgramRuleVariableTableInfo.Columns.PROGRAM_RULE_VARIABLE_SOURCE_TYPE, + ), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.kt similarity index 54% rename from core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.java rename to core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.kt index e750ed3cff..2989844be5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramSectionFields.kt @@ -25,42 +25,35 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.program.internal -package org.hisp.dhis.android.core.program.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.program.ProgramSection +import org.hisp.dhis.android.core.program.ProgramSectionTableInfo +import org.hisp.dhis.android.core.program.SectionRendering -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.program.ProgramSection; -import org.hisp.dhis.android.core.program.ProgramSectionTableInfo.Columns; -import org.hisp.dhis.android.core.program.SectionRendering; +internal object ProgramSectionFields { -public final class ProgramSectionFields { + @Deprecated("In version 2.33 and later, use {@link #TRACKED_ENTITY_ATTRIBUTES} instead.") + const val ATTRIBUTES = "programTrackedEntityAttribute" + const val TRACKED_ENTITY_ATTRIBUTES = "trackedEntityAttributes" + private const val STYLE = "style" + private const val RENDER_TYPE = "renderType" + private val fh = FieldsHelper() - /** - * @deprecated In version 2.33 and later, use {@link #TRACKED_ENTITY_ATTRIBUTES} instead. - */ - public static final String ATTRIBUTES = "programTrackedEntityAttribute"; - public static final String TRACKED_ENTITY_ATTRIBUTES = "trackedEntityAttributes"; - private static final String STYLE = "style"; - private static final String RENDER_TYPE = "renderType"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - public static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(Columns.DESCRIPTION), - fh.nestedFieldWithUid(Columns.PROGRAM), - fh.nestedFieldWithUid(ATTRIBUTES), - fh.nestedFieldWithUid(TRACKED_ENTITY_ATTRIBUTES), - fh.field(Columns.SORT_ORDER), - fh.nestedField(STYLE).with(ObjectStyleFields.allFields), - fh.field(Columns.FORM_NAME), - fh.nestedField(RENDER_TYPE) - ).build(); - - private ProgramSectionFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(ProgramSectionTableInfo.Columns.DESCRIPTION), + fh.nestedFieldWithUid(ProgramSectionTableInfo.Columns.PROGRAM), + fh.nestedFieldWithUid(ATTRIBUTES), + fh.nestedFieldWithUid(TRACKED_ENTITY_ATTRIBUTES), + fh.field(ProgramSectionTableInfo.Columns.SORT_ORDER), + fh.nestedField(STYLE).with(ObjectStyleFields.allFields), + fh.field(ProgramSectionTableInfo.Columns.FORM_NAME), + fh.nestedField(RENDER_TYPE), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.kt similarity index 58% rename from core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.java rename to core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.kt index 3d2a6fa85f..7b36caebc5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementFields.kt @@ -25,38 +25,34 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -package org.hisp.dhis.android.core.program.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.common.ValueTypeRendering; -import org.hisp.dhis.android.core.dataelement.DataElement; -import org.hisp.dhis.android.core.dataelement.internal.DataElementFields; -import org.hisp.dhis.android.core.program.ProgramStageDataElement; -import org.hisp.dhis.android.core.program.ProgramStageDataElementTableInfo.Columns; - -public final class ProgramStageDataElementFields { - public static final String RENDER_TYPE = "renderType"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(Columns.DISPLAY_IN_REPORTS), - fh.nestedField(Columns.DATA_ELEMENT).with(DataElementFields.allFields), - fh.field(Columns.COMPULSORY), - fh.field(Columns.ALLOW_PROVIDED_ELSEWHERE), - fh.field(Columns.SORT_ORDER), - fh.field(Columns.ALLOW_FUTURE_DATE), - fh.field(RENDER_TYPE), - fh.nestedField(Columns.PROGRAM_STAGE).with(ObjectWithUid.uid) - ).build(); - - - private ProgramStageDataElementFields() { - } - +package org.hisp.dhis.android.core.program.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.common.ValueTypeRendering +import org.hisp.dhis.android.core.dataelement.DataElement +import org.hisp.dhis.android.core.dataelement.internal.DataElementFields +import org.hisp.dhis.android.core.program.ProgramStageDataElement +import org.hisp.dhis.android.core.program.ProgramStageDataElementTableInfo + +object ProgramStageDataElementFields { + const val RENDER_TYPE = "renderType" + + private val fh = FieldsHelper() + + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(ProgramStageDataElementTableInfo.Columns.DISPLAY_IN_REPORTS), + fh.nestedField(ProgramStageDataElementTableInfo.Columns.DATA_ELEMENT) + .with(DataElementFields.allFields), + fh.field(ProgramStageDataElementTableInfo.Columns.COMPULSORY), + fh.field(ProgramStageDataElementTableInfo.Columns.ALLOW_PROVIDED_ELSEWHERE), + fh.field(ProgramStageDataElementTableInfo.Columns.SORT_ORDER), + fh.field(ProgramStageDataElementTableInfo.Columns.ALLOW_FUTURE_DATE), + fh.field(RENDER_TYPE), + fh.nestedField(ProgramStageDataElementTableInfo.Columns.PROGRAM_STAGE) + .with(ObjectWithUid.uid), + ).build() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.java deleted file mode 100644 index da87cb9c87..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.program.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.attribute.AttributeValue; -import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.FeatureType; -import org.hisp.dhis.android.core.common.FormType; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.common.ValidationStrategy; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.internal.DataAccessFields; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.period.PeriodType; -import org.hisp.dhis.android.core.program.ProgramStage; -import org.hisp.dhis.android.core.program.ProgramStageDataElement; -import org.hisp.dhis.android.core.program.ProgramStageSection; -import org.hisp.dhis.android.core.program.ProgramStageTableInfo.Columns; - -public final class ProgramStageFields { - - private static final String PROGRAM_STAGE_DATA_ELEMENTS = "programStageDataElements"; - private static final String CAPTURE_COORDINATES = "captureCoordinates"; - private static final String STYLE = "style"; - static final String PROGRAM_STAGE_SECTIONS = "programStageSections"; - public static final String ATTRIBUTE_VALUES = "attributeValues"; - private static final String ACCESS = "access"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - static final Field uid = fh.uid(); - static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(Columns.DESCRIPTION), - fh.field(Columns.DISPLAY_DESCRIPTION), - fh.field(Columns.EXECUTION_DATE_LABEL), - fh.field(Columns.DUE_DATE_LABEL), - fh.field(Columns.ALLOW_GENERATE_NEXT_VISIT), - fh.field(Columns.VALID_COMPLETE_ONLY), - fh.field(Columns.REPORT_DATE_TO_USE), - fh.field(Columns.OPEN_AFTER_ENROLLMENT), - fh.field(Columns.REPEATABLE), - fh.field(CAPTURE_COORDINATES), - fh.field(Columns.FEATURE_TYPE), - fh.field(Columns.FORM_TYPE), - fh.field(Columns.DISPLAY_GENERATE_EVENT_BOX), - fh.field(Columns.GENERATED_BY_ENROLMENT_DATE), - fh.field(Columns.AUTO_GENERATE_EVENT), - fh.field(Columns.SORT_ORDER), - fh.field(Columns.HIDE_DUE_DATE), - fh.field(Columns.BLOCK_ENTRY_FORM), - fh.field(Columns.MIN_DAYS_FROM_START), - fh.field(Columns.STANDARD_INTERVAL), - fh.nestedField(PROGRAM_STAGE_SECTIONS) - .with(ProgramStageSectionFields.allFields), - fh.nestedField(PROGRAM_STAGE_DATA_ELEMENTS) - .with(ProgramStageDataElementFields.allFields), - fh.nestedField(STYLE).with(ObjectStyleFields.allFields), - fh.field(Columns.PERIOD_TYPE), - fh.field(Columns.PROGRAM), - fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), - fh.field(Columns.REMIND_COMPLETED), - fh.field(Columns.VALIDATION_STRATEGY), - fh.field(Columns.ENABLE_USER_ASSIGNMENT), - fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields) - ).build(); - - private ProgramStageFields() { - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt new file mode 100644 index 0000000000..852fccc03e --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.program.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.attribute.AttributeValue +import org.hisp.dhis.android.core.attribute.internal.AttributeValuesFields +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.FormType +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.common.ValidationStrategy +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.internal.DataAccessFields +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.android.core.program.ProgramStageDataElement +import org.hisp.dhis.android.core.program.ProgramStageSection +import org.hisp.dhis.android.core.program.ProgramStageTableInfo + +internal object ProgramStageFields { + private const val PROGRAM_STAGE_DATA_ELEMENTS = "programStageDataElements" + private const val CAPTURE_COORDINATES = "captureCoordinates" + private const val STYLE = "style" + const val PROGRAM_STAGE_SECTIONS = "programStageSections" + const val ATTRIBUTE_VALUES = "attributeValues" + private const val ACCESS = "access" + + private val fh = FieldsHelper() + + val uid = fh.uid() + + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(ProgramStageTableInfo.Columns.DESCRIPTION), + fh.field(ProgramStageTableInfo.Columns.DISPLAY_DESCRIPTION), + fh.field(ProgramStageTableInfo.Columns.EXECUTION_DATE_LABEL), + fh.field(ProgramStageTableInfo.Columns.DUE_DATE_LABEL), + fh.field(ProgramStageTableInfo.Columns.ALLOW_GENERATE_NEXT_VISIT), + fh.field(ProgramStageTableInfo.Columns.VALID_COMPLETE_ONLY), + fh.field(ProgramStageTableInfo.Columns.REPORT_DATE_TO_USE), + fh.field(ProgramStageTableInfo.Columns.OPEN_AFTER_ENROLLMENT), + fh.field(ProgramStageTableInfo.Columns.REPEATABLE), + fh.field(CAPTURE_COORDINATES), + fh.field(ProgramStageTableInfo.Columns.FEATURE_TYPE), + fh.field(ProgramStageTableInfo.Columns.FORM_TYPE), + fh.field(ProgramStageTableInfo.Columns.DISPLAY_GENERATE_EVENT_BOX), + fh.field(ProgramStageTableInfo.Columns.GENERATED_BY_ENROLMENT_DATE), + fh.field(ProgramStageTableInfo.Columns.AUTO_GENERATE_EVENT), + fh.field(ProgramStageTableInfo.Columns.SORT_ORDER), + fh.field(ProgramStageTableInfo.Columns.HIDE_DUE_DATE), + fh.field(ProgramStageTableInfo.Columns.BLOCK_ENTRY_FORM), + fh.field(ProgramStageTableInfo.Columns.MIN_DAYS_FROM_START), + fh.field(ProgramStageTableInfo.Columns.STANDARD_INTERVAL), + fh.nestedField(PROGRAM_STAGE_SECTIONS) + .with(ProgramStageSectionFields.allFields), + fh.nestedField(PROGRAM_STAGE_DATA_ELEMENTS) + .with(ProgramStageDataElementFields.allFields), + fh.nestedField(STYLE).with(ObjectStyleFields.allFields), + fh.field(ProgramStageTableInfo.Columns.PERIOD_TYPE), + fh.field(ProgramStageTableInfo.Columns.PROGRAM), + fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.write)), + fh.field(ProgramStageTableInfo.Columns.REMIND_COMPLETED), + fh.field(ProgramStageTableInfo.Columns.VALIDATION_STRATEGY), + fh.field(ProgramStageTableInfo.Columns.ENABLE_USER_ASSIGNMENT), + fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.kt similarity index 60% rename from core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.java rename to core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.kt index d8aa1a4756..2514b021bf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageSectionFields.kt @@ -25,34 +25,29 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.program.internal -package org.hisp.dhis.android.core.program.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.program.ProgramStageSection +import org.hisp.dhis.android.core.program.ProgramStageSectionTableInfo +import org.hisp.dhis.android.core.program.SectionRendering -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.program.ProgramStageSection; -import org.hisp.dhis.android.core.program.SectionRendering; -import org.hisp.dhis.android.core.program.ProgramStageSectionTableInfo.Columns; +internal object ProgramStageSectionFields { + const val PROGRAM_INDICATORS = "programIndicators" + const val DATA_ELEMENTS = "dataElements" + private const val RENDER_TYPE = "renderType" -public final class ProgramStageSectionFields { + private val fh = FieldsHelper() - public static final String PROGRAM_INDICATORS = "programIndicators"; - public static final String DATA_ELEMENTS = "dataElements"; - private static final String RENDER_TYPE = "renderType"; - - private static final FieldsHelper fh = new FieldsHelper<>(); - - public static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(Columns.SORT_ORDER), - fh.nestedFieldWithUid(PROGRAM_INDICATORS), - fh.nestedFieldWithUid(DATA_ELEMENTS), - fh.nestedField(RENDER_TYPE), - fh.field(Columns.DESCRIPTION), - fh.field(Columns.DISPLAY_DESCRIPTION) - ).build(); - - private ProgramStageSectionFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(ProgramStageSectionTableInfo.Columns.SORT_ORDER), + fh.nestedFieldWithUid(PROGRAM_INDICATORS), + fh.nestedFieldWithUid(DATA_ELEMENTS), + fh.nestedField(RENDER_TYPE), + fh.field(ProgramStageSectionTableInfo.Columns.DESCRIPTION), + fh.field(ProgramStageSectionTableInfo.Columns.DISPLAY_DESCRIPTION), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.kt similarity index 60% rename from core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.java rename to core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.kt index 8c65c21282..e68eba2ac1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramTrackedEntityAttributeFields.kt @@ -25,34 +25,28 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.program.internal -package org.hisp.dhis.android.core.program.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.ValueTypeRendering +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttributeTableInfo -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.ValueTypeRendering; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttributeTableInfo.Columns; +internal object ProgramTrackedEntityAttributeFields { + const val RENDER_TYPE = "renderType" + private val fh = FieldsHelper() -public final class ProgramTrackedEntityAttributeFields { - public static final String RENDER_TYPE = "renderType"; - - private static FieldsHelper fh = new FieldsHelper<>(); - - public static final Fields allFields - = Fields.builder() - .fields(fh.getNameableFields()) - .fields( - fh.field(Columns.MANDATORY), - fh.nestedFieldWithUid(Columns.PROGRAM), - fh.field(Columns.ALLOW_FUTURE_DATE), - fh.field(Columns.DISPLAY_IN_LIST), - fh.field(Columns.SORT_ORDER), - fh.field(Columns.SEARCHABLE), - fh.nestedFieldWithUid(Columns.TRACKED_ENTITY_ATTRIBUTE), - fh.field(RENDER_TYPE) - ).build(); - - private ProgramTrackedEntityAttributeFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields(fh.getNameableFields()) + .fields( + fh.field(ProgramTrackedEntityAttributeTableInfo.Columns.MANDATORY), + fh.nestedFieldWithUid(ProgramTrackedEntityAttributeTableInfo.Columns.PROGRAM), + fh.field(ProgramTrackedEntityAttributeTableInfo.Columns.ALLOW_FUTURE_DATE), + fh.field(ProgramTrackedEntityAttributeTableInfo.Columns.DISPLAY_IN_LIST), + fh.field(ProgramTrackedEntityAttributeTableInfo.Columns.SORT_ORDER), + fh.field(ProgramTrackedEntityAttributeTableInfo.Columns.SEARCHABLE), + fh.nestedFieldWithUid(ProgramTrackedEntityAttributeTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE), + fh.field(RENDER_TYPE), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.kt similarity index 52% rename from core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.java rename to core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.kt index 3b86021076..e47d5f60d2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipTypeFields.kt @@ -25,49 +25,43 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.relationship.internal -package org.hisp.dhis.android.core.relationship.internal; +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.internal.DataAccessFields +import org.hisp.dhis.android.core.relationship.RelationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.android.core.relationship.RelationshipTypeTableInfo -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.internal.DataAccessFields; -import org.hisp.dhis.android.core.relationship.RelationshipConstraint; -import org.hisp.dhis.android.core.relationship.RelationshipType; -import org.hisp.dhis.android.core.relationship.RelationshipTypeTableInfo.Columns; - -public final class RelationshipTypeFields { - - private static final String B_IS_TO_A = "bIsToA"; - private static final String A_IS_TO_B = "aIsToB"; - private static final String FROM_CONSTRAINT = "fromConstraint"; - private static final String TO_CONSTRAINT = "toConstraint"; - private static final String ACCESS = "access"; +internal object RelationshipTypeFields { + private const val B_IS_TO_A = "bIsToA" + private const val A_IS_TO_B = "aIsToB" + private const val FROM_CONSTRAINT = "fromConstraint" + private const val TO_CONSTRAINT = "toConstraint" + private const val ACCESS = "access" // Used only for children appending, can't be used in query - public static final String CONSTRAINTS = "constraints"; - - private static final FieldsHelper fh = new FieldsHelper<>(); + const val CONSTRAINTS = "constraints" - static final Field lastUpdated = fh.lastUpdated(); + private val fh = FieldsHelper() - static final Fields allFields = Fields.builder() - .fields(fh.getIdentifiableFields()) - .fields( - fh.field(B_IS_TO_A), - fh.field(A_IS_TO_B), - fh.field(Columns.FROM_TO_NAME), - fh.field(Columns.TO_FROM_NAME), - fh.field(Columns.BIDIRECTIONAL), - fh.nestedField(FROM_CONSTRAINT) - .with(RelationshipConstraintFields.allFields), - fh.nestedField(TO_CONSTRAINT) - .with(RelationshipConstraintFields.allFields), - fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.allFields)) - ).build(); + val lastUpdated = fh.lastUpdated() - private RelationshipTypeFields() { - } -} \ No newline at end of file + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(B_IS_TO_A), + fh.field(A_IS_TO_B), + fh.field(RelationshipTypeTableInfo.Columns.FROM_TO_NAME), + fh.field(RelationshipTypeTableInfo.Columns.TO_FROM_NAME), + fh.field(RelationshipTypeTableInfo.Columns.BIDIRECTIONAL), + fh.nestedField(FROM_CONSTRAINT) + .with(RelationshipConstraintFields.allFields), + fh.nestedField(TO_CONSTRAINT) + .with(RelationshipConstraintFields.allFields), + fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.allFields)), + ).build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.kt similarity index 56% rename from core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.java rename to core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.kt index f9adc10df2..6a6385ac83 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityTypeFields.kt @@ -25,44 +25,37 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -package org.hisp.dhis.android.core.trackedentity.internal; - -import org.hisp.dhis.android.core.arch.api.fields.internal.Field; -import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; -import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper; -import org.hisp.dhis.android.core.common.Access; -import org.hisp.dhis.android.core.common.FeatureType; -import org.hisp.dhis.android.core.common.ObjectStyle; -import org.hisp.dhis.android.core.common.internal.AccessFields; -import org.hisp.dhis.android.core.common.internal.DataAccessFields; -import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityType; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeAttribute; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeTableInfo.Columns; - -public final class TrackedEntityTypeFields { - - private final static String STYLE = "style"; - public final static String TRACKED_ENTITY_TYPE_ATTRIBUTES = "trackedEntityTypeAttributes"; - private static final String ACCESS = "access"; - - private static final FieldsHelper fh = new FieldsHelper<>(); - - public static final Field uid = fh.uid(); - - static final Field lastUpdated = fh.lastUpdated(); - - public static final Fields allFields = Fields.builder() - .fields(fh.getNameableFields()) - .fields( - fh.nestedField(TRACKED_ENTITY_TYPE_ATTRIBUTES) - .with(TrackedEntityTypeAttributeFields.allFields), - fh.nestedField(STYLE).with(ObjectStyleFields.allFields), - fh.field(Columns.FEATURE_TYPE), - fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.allFields)) - ).build(); - - private TrackedEntityTypeFields() { - } -} \ No newline at end of file +package org.hisp.dhis.android.core.trackedentity.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.common.Access +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.common.internal.DataAccessFields +import org.hisp.dhis.android.core.common.objectstyle.internal.ObjectStyleFields +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeAttribute +import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeTableInfo + +internal object TrackedEntityTypeFields { + private const val STYLE = "style" + const val TRACKED_ENTITY_TYPE_ATTRIBUTES = "trackedEntityTypeAttributes" + private const val ACCESS = "access" + private val fh = FieldsHelper() + + val uid = fh.uid() + + val lastUpdated = fh.lastUpdated() + + val allFields: Fields = Fields.builder() + .fields(fh.getNameableFields()) + .fields( + fh.nestedField(TRACKED_ENTITY_TYPE_ATTRIBUTES) + .with(TrackedEntityTypeAttributeFields.allFields), + fh.nestedField(STYLE).with(ObjectStyleFields.allFields), + fh.field(TrackedEntityTypeTableInfo.Columns.FEATURE_TYPE), + fh.nestedField(ACCESS).with(AccessFields.data.with(DataAccessFields.allFields)), + ).build() +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt new file mode 100644 index 0000000000..133be20f00 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.program + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class ProgramShould : BaseObjectShould("program/program.json"), ObjectShould { + @Test + override fun map_from_json_string() { + val program = objectMapper.readValue(jsonStream, Program::class.java) + + assertThat(program.lastUpdated()).isEqualTo(DateUtils.DATE_FORMAT.parse("2015-10-15T11:32:27.242")) + assertThat(program.created()).isEqualTo(DateUtils.DATE_FORMAT.parse("2014-06-06T20:44:21.375")) + assertThat(program.uid()).isEqualTo("WSGAb5XwJ3Y") + assertThat(program.name()).isEqualTo("WHO RMNCH Tracker") + assertThat(program.displayName()).isEqualTo("WHO RMNCH Tracker") + assertThat(program.shortName()).isEqualTo("WHO RMNCH Tracker") + assertThat(program.displayShortName()).isEqualTo("WHO RMNCH Tracker") + assertThat(program.ignoreOverdueEvents()).isFalse() + assertThat(program.dataEntryMethod()).isFalse() + assertThat(program.captureCoordinates()).isTrue() + assertThat(program.enrollmentDateLabel()).isEqualTo("Date of first visit") + assertThat(program.onlyEnrollOnce()).isFalse() + assertThat(program.version()).isEqualTo(11) + assertThat(program.selectIncidentDatesInFuture()).isTrue() + assertThat(program.incidentDateLabel()).isEqualTo("Date of incident") + assertThat(program.selectEnrollmentDatesInFuture()).isFalse() + assertThat(program.registration()).isTrue() + assertThat(program.useFirstStageDuringRegistration()).isFalse() + assertThat(program.minAttributesRequiredToSearch()).isEqualTo(3) + assertThat(program.maxTeiCountToReturn()).isEqualTo(2) + assertThat(program.featureType()).isEqualTo(FeatureType.MULTI_POLYGON) + assertThat(program.accessLevel()).isEqualTo(AccessLevel.PROTECTED) + assertThat(program.displayFrontPageList()).isFalse() + assertThat(program.programType()).isEqualTo(ProgramType.WITH_REGISTRATION) + assertThat(program.displayIncidentDate()).isFalse() + assertThat(program.categoryCombo()!!.uid()).isEqualTo("p0KPaWEg3cf") + assertThat(program.trackedEntityType()!!.uid()).isEqualTo("nEenWmSyUEp") + assertThat(program.relatedProgram()!!.uid()).isEqualTo("IpHINAT79UW") + assertThat(program.programRuleVariables()!![0].uid()).isEqualTo("varonrw1032") + assertThat(program.programRuleVariables()!![1].uid()).isEqualTo("idLCptBEOF9") + assertThat(program.programTrackedEntityAttributes()!![0].uid()).isEqualTo("YGMlKXYa5xF") + assertThat(program.programTrackedEntityAttributes()!![1].uid()).isEqualTo("WZWEBrkJSAm") + assertThat(program.programSections()!![0].uid()).isEqualTo("FdpWnXhl7c1") + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt new file mode 100644 index 0000000000..a124c1def4 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.program + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.FormType +import org.hisp.dhis.android.core.common.ObjectShould +import org.hisp.dhis.android.core.common.ValidationStrategy +import org.hisp.dhis.android.core.period.PeriodType +import org.junit.Test + +class ProgramStageShould : BaseObjectShould("program/program_stage.json"), ObjectShould { + @Test + override fun map_from_json_string() { + val programStage = objectMapper.readValue(jsonStream, ProgramStage::class.java) + + assertThat(programStage.lastUpdated()).isEqualTo(DateUtils.DATE_FORMAT.parse("2013-04-10T12:15:02.041")) + assertThat(programStage.created()).isEqualTo(DateUtils.DATE_FORMAT.parse("2013-03-04T11:41:07.541")) + assertThat(programStage.uid()).isEqualTo("eaDHS084uMp") + assertThat(programStage.name()).isEqualTo("ANC 1st visit") + assertThat(programStage.displayName()).isEqualTo("ANC 1st visit") + assertThat(programStage.description()).isEqualTo("ANC 1st visit") + assertThat(programStage.displayDescription()).isEqualTo("ANC 1st visit") + assertThat(programStage.sortOrder()).isEqualTo(1) + assertThat(programStage.allowGenerateNextVisit()).isFalse() + assertThat(programStage.autoGenerateEvent()).isTrue() + assertThat(programStage.blockEntryForm()).isFalse() + assertThat(programStage.captureCoordinates()).isTrue() + assertThat(programStage.displayGenerateEventBox()).isFalse() + assertThat(programStage.executionDateLabel()).isNull() + assertThat(programStage.dueDateLabel()).isEqualTo("Due date") + assertThat(programStage.formType()).isEqualTo(FormType.DEFAULT) + assertThat(programStage.generatedByEnrollmentDate()).isFalse() + assertThat(programStage.hideDueDate()).isFalse() + assertThat(programStage.minDaysFromStart()).isEqualTo(0) + assertThat(programStage.openAfterEnrollment()).isFalse() + assertThat(programStage.repeatable()).isFalse() + assertThat(programStage.reportDateToUse()).isEqualTo("false") + assertThat(programStage.standardInterval()).isNull() + assertThat(ProgramStageInternalAccessor.accessProgramStageSections(programStage)).isEmpty() + assertThat(programStage.periodType()).isEqualTo(PeriodType.Monthly) + assertThat(programStage.remindCompleted()).isFalse() + assertThat(programStage.validationStrategy()).isEqualTo(ValidationStrategy.ON_UPDATE_AND_INSERT) + assertThat(programStage.featureType()).isEqualTo(FeatureType.POINT) + assertThat(programStage.enableUserAssignment()).isTrue() + + val dataElements = ProgramStageInternalAccessor.accessProgramStageDataElements(programStage) + assertThat(dataElements[0].uid()).isEqualTo("EQCf1l2Mdr8") + assertThat(dataElements[1].uid()).isEqualTo("muxw4SGzUwJ") + assertThat(dataElements[2].uid()).isEqualTo("KWybjio9UZT") + assertThat(dataElements[3].uid()).isEqualTo("ejm0g2hwHHc") + assertThat(dataElements[4].uid()).isEqualTo("yvV3txhSCyc") + assertThat(dataElements[5].uid()).isEqualTo("fzQrjBpbwQD") + assertThat(dataElements[6].uid()).isEqualTo("BbvkNf9PCxX") + assertThat(dataElements[7].uid()).isEqualTo("MbdCfd4HaMQ") + assertThat(dataElements[8].uid()).isEqualTo("SBn4XCFRbyT") + assertThat(dataElements[9].uid()).isEqualTo("F0PZ4nZ86vo") + assertThat(dataElements[10].uid()).isEqualTo("gFBRqVFh60H") + assertThat(dataElements[11].uid()).isEqualTo("ljAoyjH4GYA") + assertThat(dataElements[12].uid()).isEqualTo("MAdsNY2gOlv") + assertThat(dataElements[13].uid()).isEqualTo("IpVUTCDdlGW") + assertThat(dataElements[14].uid()).isEqualTo("psBtdqepNVM") + assertThat(dataElements[15].uid()).isEqualTo("UzB6pZxZ2Rb") + assertThat(dataElements[16].uid()).isEqualTo("FQZEMbBVabW") + } +} From a2cf25b8abada306fa7838262abdd64ea8a4c9f9 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 2 Feb 2024 12:54:01 +0100 Subject: [PATCH 044/222] [ANDROSDK-1806] Add custom tracker terminology fields --- ...ectionRepositoryMockIntegrationShould.java | 48 ++++++++ ...ectionRepositoryMockIntegrationShould.java | 20 ++++ core/src/main/assets/migrations/157.sql | 11 ++ core/src/main/assets/snapshots/snapshot.sql | 4 +- .../access/internal/BaseDatabaseOpenHelper.kt | 2 +- .../dhis/android/core/program/Program.java | 36 ++++++ .../program/ProgramCollectionRepository.kt | 24 ++++ .../android/core/program/ProgramStage.java | 12 ++ .../ProgramStageCollectionRepository.kt | 8 ++ .../core/program/ProgramStageTableInfo.java | 6 +- .../core/program/ProgramTableInfo.java | 14 ++- .../program/internal/ProgramStageStoreImpl.kt | 2 + .../core/program/internal/ProgramStoreImpl.kt | 6 + .../core/data/program/ProgramSamples.java | 6 + .../data/program/ProgramStageSamples.java | 2 + .../sharedTest/resources/program/program.json | 6 + .../resources/program/program_stage.json | 2 + .../resources/program/program_stages.json | 2 + .../resources/program/programs.json | 6 + .../android/core/program/ProgramShould.java | 98 ---------------- .../android/core/program/ProgramShould.kt | 6 + .../core/program/ProgramStageShould.java | 110 ------------------ .../core/program/ProgramStageShould.kt | 2 + 23 files changed, 220 insertions(+), 213 deletions(-) create mode 100644 core/src/main/assets/migrations/157.sql delete mode 100644 core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.java delete mode 100644 core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.java diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java index 534f468396..29c9863901 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java @@ -195,6 +195,54 @@ public void filter_by_access_level() { assertThat(programs.size()).isEqualTo(1); } + @Test + public void filter_by_enrollment_label() { + List programs = d2.programModule().programs() + .byEnrollmentLabel().eq("Enrollment Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_follow_up_label() { + List programs = d2.programModule().programs() + .byFollowUpLabel().eq("Follow up Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_org_unit_label() { + List programs = d2.programModule().programs() + .byOrgUnitLabel().eq("OrgUnit Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_relationship_label() { + List programs = d2.programModule().programs() + .byRelationshipLabel().eq("Relationship Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_note_label() { + List programs = d2.programModule().programs() + .byNoteLabel().eq("Note Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_tracked_entity_attribute_label() { + List programs = d2.programModule().programs() + .byTrackedEntityAttributeLabel().eq("TrackedEntityAttribute Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + @Test public void filter_by_field_color() { List programs = d2.programModule().programs() diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java index ad08986350..e8114b0319 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java @@ -330,6 +330,26 @@ public void filter_by_validation_strategy() { assertThat(programStages.size()).isEqualTo(1); } + @Test + public void filter_by_program_stage_label() { + List programStages = + d2.programModule().programStages() + .byProgramStageLabel().eq("ProgramStage Label") + .blockingGet(); + + assertThat(programStages.size()).isEqualTo(1); + } + + @Test + public void filter_by_event_label() { + List programStages = + d2.programModule().programStages() + .byEventLabel().eq("Event Label") + .blockingGet(); + + assertThat(programStages.size()).isEqualTo(1); + } + @Test public void filter_by_field_color() { List programStages = d2.programModule().programStages() diff --git a/core/src/main/assets/migrations/157.sql b/core/src/main/assets/migrations/157.sql new file mode 100644 index 0000000000..e299a6a117 --- /dev/null +++ b/core/src/main/assets/migrations/157.sql @@ -0,0 +1,11 @@ +# Add tracker terminology (ANDROSDK-1806) + +ALTER TABLE Program ADD COLUMN enrollmentLabel TEXT; +ALTER TABLE Program ADD COLUMN followUpLabel TEXT; +ALTER TABLE Program ADD COLUMN orgUnitLabel TEXT; +ALTER TABLE Program ADD COLUMN relationshipLabel TEXT; +ALTER TABLE Program ADD COLUMN noteLabel TEXT; +ALTER TABLE Program ADD COLUMN trackedEntityAttributeLabel TEXT; + +ALTER TABLE ProgramStage ADD COLUMN programStageLabel TEXT; +ALTER TABLE ProgramStage ADD COLUMN eventLabel TEXT; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 810ae6dc45..f6fdcef9de 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -65,8 +65,8 @@ CREATE TABLE TrackerImportConflict (_id INTEGER PRIMARY KEY AUTOINCREMENT, confl CREATE TABLE DataSetOrganisationUnitLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, dataSet TEXT NOT NULL, organisationUnit TEXT NOT NULL, FOREIGN KEY (dataSet) REFERENCES DataSet (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (organisationUnit, dataSet)); CREATE TABLE UserOrganisationUnit (_id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT NOT NULL, organisationUnit TEXT NOT NULL, organisationUnitScope TEXT NOT NULL, root INTEGER, userAssigned INTEGER, FOREIGN KEY (user) REFERENCES User (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (organisationUnitScope, user, organisationUnit)); CREATE TABLE RelationshipType (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, fromToName TEXT, toFromName TEXT, bidirectional INTEGER, accessDataWrite INTEGER ); -CREATE TABLE ProgramStage (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, executionDateLabel TEXT, allowGenerateNextVisit INTEGER, validCompleteOnly INTEGER, reportDateToUse TEXT, openAfterEnrollment INTEGER, repeatable INTEGER, formType TEXT, displayGenerateEventBox INTEGER, generatedByEnrollmentDate INTEGER, autoGenerateEvent INTEGER, sortOrder INTEGER, hideDueDate INTEGER, blockEntryForm INTEGER, minDaysFromStart INTEGER, standardInterval INTEGER, program TEXT NOT NULL, periodType TEXT, accessDataWrite INTEGER, remindCompleted INTEGER, description TEXT, displayDescription TEXT, featureType TEXT, color TEXT, icon TEXT, enableUserAssignment INTEGER, dueDateLabel TEXT, validationStrategy TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE Program (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, shortName TEXT, displayShortName TEXT, description TEXT, displayDescription TEXT, version INTEGER, onlyEnrollOnce INTEGER, enrollmentDateLabel TEXT, displayIncidentDate INTEGER, incidentDateLabel TEXT, registration INTEGER, selectEnrollmentDatesInFuture INTEGER, dataEntryMethod INTEGER, ignoreOverdueEvents INTEGER, selectIncidentDatesInFuture INTEGER, useFirstStageDuringRegistration INTEGER, displayFrontPageList INTEGER, programType TEXT, relatedProgram TEXT, trackedEntityType TEXT, categoryCombo TEXT, accessDataWrite INTEGER, expiryDays INTEGER, completeEventsExpiryDays INTEGER, expiryPeriodType TEXT, minAttributesRequiredToSearch INTEGER, maxTeiCountToReturn INTEGER, featureType TEXT, accessLevel TEXT, color TEXT, icon TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (categoryCombo) REFERENCES CategoryCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE ProgramStage (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, executionDateLabel TEXT, allowGenerateNextVisit INTEGER, validCompleteOnly INTEGER, reportDateToUse TEXT, openAfterEnrollment INTEGER, repeatable INTEGER, formType TEXT, displayGenerateEventBox INTEGER, generatedByEnrollmentDate INTEGER, autoGenerateEvent INTEGER, sortOrder INTEGER, hideDueDate INTEGER, blockEntryForm INTEGER, minDaysFromStart INTEGER, standardInterval INTEGER, program TEXT NOT NULL, periodType TEXT, accessDataWrite INTEGER, remindCompleted INTEGER, description TEXT, displayDescription TEXT, featureType TEXT, color TEXT, icon TEXT, enableUserAssignment INTEGER, dueDateLabel TEXT, validationStrategy TEXT, programStageLabel TEXT, eventLabel TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE Program (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, shortName TEXT, displayShortName TEXT, description TEXT, displayDescription TEXT, version INTEGER, onlyEnrollOnce INTEGER, enrollmentDateLabel TEXT, displayIncidentDate INTEGER, incidentDateLabel TEXT, registration INTEGER, selectEnrollmentDatesInFuture INTEGER, dataEntryMethod INTEGER, ignoreOverdueEvents INTEGER, selectIncidentDatesInFuture INTEGER, useFirstStageDuringRegistration INTEGER, displayFrontPageList INTEGER, programType TEXT, relatedProgram TEXT, trackedEntityType TEXT, categoryCombo TEXT, accessDataWrite INTEGER, expiryDays INTEGER, completeEventsExpiryDays INTEGER, expiryPeriodType TEXT, minAttributesRequiredToSearch INTEGER, maxTeiCountToReturn INTEGER, featureType TEXT, accessLevel TEXT, color TEXT, icon TEXT, enrollmentLabel TEXT, followUpLabel TEXT, orgUnitLabel TEXT, relationshipLabel TEXT, noteLabel TEXT, trackedEntityAttributeLabel TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (categoryCombo) REFERENCES CategoryCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE TrackedEntityInstance (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, organisationUnit TEXT, trackedEntityType TEXT, geometryType TEXT, geometryCoordinates TEXT, syncState TEXT, aggregatedSyncState TEXT, deleted INTEGER, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE Enrollment (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, organisationUnit TEXT NOT NULL, program TEXT NOT NULL, enrollmentDate TEXT, incidentDate TEXT, followup INTEGER, status TEXT, trackedEntityInstance TEXT NOT NULL, syncState TEXT, aggregatedSyncState TEXT, geometryType TEXT, geometryCoordinates TEXT, deleted INTEGER, completedDate TEXT, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityInstance) REFERENCES TrackedEntityInstance (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE Event (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, enrollment TEXT, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, status TEXT, geometryType TEXT, geometryCoordinates TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, organisationUnit TEXT NOT NULL, eventDate TEXT, completedDate TEXT, dueDate TEXT, syncState TEXT, aggregatedSyncState TEXT, attributeOptionCombo TEXT, deleted INTEGER, assignedUser TEXT, completedBy TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (enrollment) REFERENCES Enrollment (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (attributeOptionCombo) REFERENCES CategoryOptionCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index c503e20202..6226e4b1fd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 156 + const val VERSION = 157 } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java index 833bd98751..f5a4afbe6d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java @@ -203,6 +203,30 @@ public String categoryComboUid() { @ColumnAdapter(AccessLevelColumnAdapter.class) public abstract AccessLevel accessLevel(); + @Nullable + @JsonProperty() + public abstract String enrollmentLabel(); + + @Nullable + @JsonProperty() + public abstract String followUpLabel(); + + @Nullable + @JsonProperty() + public abstract String orgUnitLabel(); + + @Nullable + @JsonProperty() + public abstract String relationshipLabel(); + + @Nullable + @JsonProperty() + public abstract String noteLabel(); + + @Nullable + @JsonProperty() + public abstract String trackedEntityAttributeLabel(); + @Nullable @JsonProperty() @ColumnAdapter(IgnoreAttributeValuesListAdapter.class) @@ -286,6 +310,18 @@ abstract Builder programTrackedEntityAttributes(List attributeValues); abstract Program autoBuild(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt index 795b056105..ba494fea01 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt @@ -165,6 +165,30 @@ class ProgramCollectionRepository internal constructor( return cf.enumC(ProgramTableInfo.Columns.ACCESS_LEVEL) } + fun byEnrollmentLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.ENROLLMENT_LABEL) + } + + fun byFollowUpLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.FOLLOW_UP_LABEL) + } + + fun byOrgUnitLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.ORG_UNIT_LABEL) + } + + fun byRelationshipLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.RELATIONSHIP_LABEL) + } + + fun byNoteLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.NOTE_LABEL) + } + + fun byTrackedEntityAttributeLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE_LABEL) + } + fun byColor(): StringFilterConnector { return cf.string(ProgramTableInfo.Columns.COLOR) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java index 827a87ecbb..e71fd0f262 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStage.java @@ -193,6 +193,14 @@ public abstract class ProgramStage extends BaseIdentifiableObject @ColumnAdapter(ValidationStrategyColumnAdapter.class) public abstract ValidationStrategy validationStrategy(); + @Nullable + @JsonProperty + public abstract String programStageLabel(); + + @Nullable + @JsonProperty + public abstract String eventLabel(); + @Nullable @JsonProperty() @ColumnAdapter(IgnoreAttributeValuesListAdapter.class) @@ -274,6 +282,10 @@ public abstract static class Builder extends BaseIdentifiableObject.Builder attributeValues); abstract ProgramStage autoBuild(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageCollectionRepository.kt index 9b27c2806b..078fa17a46 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageCollectionRepository.kt @@ -167,6 +167,14 @@ class ProgramStageCollectionRepository internal constructor( return cf.enumC(ProgramStageTableInfo.Columns.VALIDATION_STRATEGY) } + fun byProgramStageLabel(): StringFilterConnector { + return cf.string(ProgramStageTableInfo.Columns.PROGRAM_STAGE_LABEL) + } + + fun byEventLabel(): StringFilterConnector { + return cf.string(ProgramStageTableInfo.Columns.EVENT_LABEL) + } + fun byColor(): StringFilterConnector { return cf.string(ProgramStageTableInfo.Columns.COLOR) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageTableInfo.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageTableInfo.java index 7eef0836ff..997b2841ef 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageTableInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramStageTableInfo.java @@ -77,6 +77,8 @@ public static class Columns extends IdentifiableWithStyleColumns { public static final String VALIDATION_STRATEGY = "validationStrategy"; public static final String ACCESS_DATA_WRITE = "accessDataWrite"; public static final String ENABLE_USER_ASSIGNMENT = "enableUserAssignment"; + public static final String PROGRAM_STAGE_LABEL = "programStageLabel"; + public static final String EVENT_LABEL = "eventLabel"; @Override public String[] all() { @@ -105,7 +107,9 @@ public String[] all() { REMIND_COMPLETED, FEATURE_TYPE, ENABLE_USER_ASSIGNMENT, - VALIDATION_STRATEGY + VALIDATION_STRATEGY, + PROGRAM_STAGE_LABEL, + EVENT_LABEL ); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java index 90852e5b78..ed5674245b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java @@ -75,6 +75,12 @@ public static class Columns extends NameableWithStyleColumns { public static final String MAX_TEI_COUNT_TO_RETURN = "maxTeiCountToReturn"; public static final String FEATURE_TYPE = "featureType"; public static final String ACCESS_LEVEL = "accessLevel"; + public static final String ENROLLMENT_LABEL = "enrollmentLabel"; + public static final String FOLLOW_UP_LABEL = "followUpLabel"; + public static final String ORG_UNIT_LABEL = "orgUnitLabel"; + public static final String RELATIONSHIP_LABEL = "relationshipLabel"; + public static final String NOTE_LABEL = "noteLabel"; + public static final String TRACKED_ENTITY_ATTRIBUTE_LABEL = "trackedEntityAttributeLabel"; @Override public String[] all() { @@ -102,7 +108,13 @@ public String[] all() { MIN_ATTRIBUTES_REQUIRED_TO_SEARCH, MAX_TEI_COUNT_TO_RETURN, FEATURE_TYPE, - ACCESS_LEVEL + ACCESS_LEVEL, + ENROLLMENT_LABEL, + FOLLOW_UP_LABEL, + ORG_UNIT_LABEL, + RELATIONSHIP_LABEL, + NOTE_LABEL, + TRACKED_ENTITY_ATTRIBUTE_LABEL ); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageStoreImpl.kt index d1b6e976f6..2470e3bb87 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageStoreImpl.kt @@ -80,6 +80,8 @@ internal class ProgramStageStoreImpl( w.bind(31, o.featureType()) w.bind(32, o.enableUserAssignment()) w.bind(33, o.validationStrategy()) + w.bind(34, o.programStageLabel()) + w.bind(35, o.eventLabel()) } } val CHILD_PROJECTION = SingleParentChildProjection( diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt index 1fb5c4dbcf..db751d727f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt @@ -87,6 +87,12 @@ internal class ProgramStoreImpl( w.bind(34, o.maxTeiCountToReturn()) w.bind(35, o.featureType()) w.bind(36, o.accessLevel()) + w.bind(37, o.enrollmentLabel()) + w.bind(38, o.followUpLabel()) + w.bind(39, o.orgUnitLabel()) + w.bind(40, o.relationshipLabel()) + w.bind(41, o.noteLabel()) + w.bind(42, o.trackedEntityAttributeLabel()) } } } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java index 15519dfbd4..b4cf812edc 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java @@ -75,6 +75,12 @@ public static Program getProgram() { .maxTeiCountToReturn(2) .featureType(FeatureType.POINT) .accessLevel(AccessLevel.PROTECTED) + .enrollmentLabel("enrollmentLabel") + .followUpLabel("followUpLabel") + .orgUnitLabel("orgUnitLabel") + .relationshipLabel("relationshipLabel") + .noteLabel("noteLabel") + .trackedEntityAttributeLabel("trackedEntityAttributeLabel") .build(); return builder.build(); } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramStageSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramStageSamples.java index 7af8bbfcfd..be0ce2ac32 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramStageSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramStageSamples.java @@ -73,6 +73,8 @@ public static ProgramStage getProgramStage() { .access(Access.create(false, false, DataAccess.create(true, true))) .remindCompleted(Boolean.FALSE) .validationStrategy(ValidationStrategy.ON_UPDATE_AND_INSERT) + .programStageLabel("programStageLabel") + .eventLabel("eventLabel") .build(); } diff --git a/core/src/sharedTest/resources/program/program.json b/core/src/sharedTest/resources/program/program.json index d189fa8682..9c33d95bf1 100644 --- a/core/src/sharedTest/resources/program/program.json +++ b/core/src/sharedTest/resources/program/program.json @@ -14,6 +14,12 @@ "version": 11, "selectIncidentDatesInFuture": true, "incidentDateLabel": "Date of incident", + "enrollmentLabel": "Enrollment Label", + "followUpLabel": "Follow up Label", + "orgUnitLabel": "OrgUnit Label", + "relationshipLabel": "Relationship Label", + "noteLabel": "Note Label", + "trackedEntityAttributeLabel": "TrackedEntityAttribute Label", "selectEnrollmentDatesInFuture": false, "registration": true, "useFirstStageDuringRegistration": false, diff --git a/core/src/sharedTest/resources/program/program_stage.json b/core/src/sharedTest/resources/program/program_stage.json index ecbcf2786f..ce692cee7b 100644 --- a/core/src/sharedTest/resources/program/program_stage.json +++ b/core/src/sharedTest/resources/program/program_stage.json @@ -18,6 +18,8 @@ "captureCoordinates": true, "formType": "DEFAULT", "dueDateLabel": "Due date", + "programStageLabel": "ProgramStage Label", + "eventLabel": "Event Label", "remindCompleted": false, "displayGenerateEventBox": false, "validationStrategy": "ON_UPDATE_AND_INSERT", diff --git a/core/src/sharedTest/resources/program/program_stages.json b/core/src/sharedTest/resources/program/program_stages.json index a633ed0b74..807cba967b 100644 --- a/core/src/sharedTest/resources/program/program_stages.json +++ b/core/src/sharedTest/resources/program/program_stages.json @@ -10,6 +10,8 @@ "allowGenerateNextVisit": false, "executionDateLabel": "Visit date", "dueDateLabel": "Due date", + "programStageLabel": "ProgramStage Label", + "eventLabel": "Event Label", "validCompleteOnly": true, "displayName": "Antenatal care visit - Program rules demo", "openAfterEnrollment": false, diff --git a/core/src/sharedTest/resources/program/programs.json b/core/src/sharedTest/resources/program/programs.json index 752ae83dc6..0b130ffabe 100644 --- a/core/src/sharedTest/resources/program/programs.json +++ b/core/src/sharedTest/resources/program/programs.json @@ -20,6 +20,12 @@ "version": 3, "selectIncidentDatesInFuture": false, "incidentDateLabel": "Incident Date", + "enrollmentLabel": "Enrollment Label", + "followUpLabel": "Follow up Label", + "orgUnitLabel": "OrgUnit Label", + "relationshipLabel": "Relationship Label", + "noteLabel": "Note Label", + "trackedEntityAttributeLabel": "TrackedEntityAttribute Label", "displayIncidentDate": false, "selectEnrollmentDatesInFuture": false, "registration": false, diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.java b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.java deleted file mode 100644 index 7acc2a357c..0000000000 --- a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.program; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.hisp.dhis.android.core.Inject; -import org.hisp.dhis.android.core.common.BaseIdentifiableObject; -import org.hisp.dhis.android.core.common.BaseObjectShould; -import org.hisp.dhis.android.core.common.FeatureType; -import org.hisp.dhis.android.core.common.ObjectShould; -import org.junit.Test; - -import java.io.IOException; -import java.text.ParseException; - -import static com.google.common.truth.Truth.assertThat; - -public class ProgramShould extends BaseObjectShould implements ObjectShould { - - public ProgramShould() { - super("program/program.json"); - } - - @Override - @Test - public void map_from_json_string() throws IOException, ParseException { - ObjectMapper objectMapper = Inject.objectMapper(); - Program program = objectMapper.readValue(jsonStream, Program.class); - - assertThat(program.lastUpdated()).isEqualTo( - BaseIdentifiableObject.DATE_FORMAT.parse("2015-10-15T11:32:27.242")); - assertThat(program.created()).isEqualTo( - BaseIdentifiableObject.DATE_FORMAT.parse("2014-06-06T20:44:21.375")); - - assertThat(program.uid()).isEqualTo("WSGAb5XwJ3Y"); - assertThat(program.name()).isEqualTo("WHO RMNCH Tracker"); - assertThat(program.displayName()).isEqualTo("WHO RMNCH Tracker"); - assertThat(program.shortName()).isEqualTo("WHO RMNCH Tracker"); - assertThat(program.displayShortName()).isEqualTo("WHO RMNCH Tracker"); - assertThat(program.ignoreOverdueEvents()).isFalse(); - assertThat(program.dataEntryMethod()).isFalse(); - assertThat(program.captureCoordinates()).isTrue(); - assertThat(program.enrollmentDateLabel()).isEqualTo("Date of first visit"); - assertThat(program.onlyEnrollOnce()).isFalse(); - assertThat(program.version()).isEqualTo(11); - assertThat(program.selectIncidentDatesInFuture()).isTrue(); - assertThat(program.incidentDateLabel()).isEqualTo("Date of incident"); - assertThat(program.selectEnrollmentDatesInFuture()).isFalse(); - assertThat(program.registration()).isTrue(); - assertThat(program.useFirstStageDuringRegistration()).isFalse(); - assertThat(program.minAttributesRequiredToSearch()).isEqualTo(3); - assertThat(program.maxTeiCountToReturn()).isEqualTo(2); - assertThat(program.featureType()).isEqualTo(FeatureType.MULTI_POLYGON); - assertThat(program.accessLevel()).isEqualTo(AccessLevel.PROTECTED); - - assertThat(program.displayFrontPageList()).isFalse(); - assertThat(program.programType()).isEqualTo(ProgramType.WITH_REGISTRATION); - assertThat(program.displayIncidentDate()).isFalse(); - assertThat(program.categoryCombo().uid()).isEqualTo("p0KPaWEg3cf"); - assertThat(program.trackedEntityType().uid()).isEqualTo("nEenWmSyUEp"); - assertThat(program.relatedProgram().uid()).isEqualTo("IpHINAT79UW"); - - assertThat(program.programRuleVariables().get(0).uid()).isEqualTo("varonrw1032"); - assertThat(program.programRuleVariables().get(1).uid()).isEqualTo("idLCptBEOF9"); - - assertThat(program.programTrackedEntityAttributes().get(0).uid()).isEqualTo("YGMlKXYa5xF"); - assertThat(program.programTrackedEntityAttributes().get(1).uid()).isEqualTo("WZWEBrkJSAm"); - - assertThat(program.programSections().get(0).uid()).isEqualTo("FdpWnXhl7c1"); - } -} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt index 133be20f00..5b55c17c63 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt @@ -61,6 +61,12 @@ class ProgramShould : BaseObjectShould("program/program.json"), ObjectShould { assertThat(program.maxTeiCountToReturn()).isEqualTo(2) assertThat(program.featureType()).isEqualTo(FeatureType.MULTI_POLYGON) assertThat(program.accessLevel()).isEqualTo(AccessLevel.PROTECTED) + assertThat(program.enrollmentLabel()).isEqualTo("Enrollment Label") + assertThat(program.followUpLabel()).isEqualTo("Follow up Label") + assertThat(program.orgUnitLabel()).isEqualTo("OrgUnit Label") + assertThat(program.relationshipLabel()).isEqualTo("Relationship Label") + assertThat(program.noteLabel()).isEqualTo("Note Label") + assertThat(program.trackedEntityAttributeLabel()).isEqualTo("TrackedEntityAttribute Label") assertThat(program.displayFrontPageList()).isFalse() assertThat(program.programType()).isEqualTo(ProgramType.WITH_REGISTRATION) assertThat(program.displayIncidentDate()).isFalse() diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.java b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.java deleted file mode 100644 index 1be79d6f4f..0000000000 --- a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.program; - -import org.hisp.dhis.android.core.common.BaseIdentifiableObject; -import org.hisp.dhis.android.core.common.BaseObjectShould; -import org.hisp.dhis.android.core.common.FeatureType; -import org.hisp.dhis.android.core.common.FormType; -import org.hisp.dhis.android.core.common.ObjectShould; -import org.hisp.dhis.android.core.common.ValidationStrategy; -import org.hisp.dhis.android.core.period.PeriodType; -import org.junit.Test; - -import java.io.IOException; -import java.text.ParseException; -import java.util.List; - -import static com.google.common.truth.Truth.assertThat; - -public class ProgramStageShould extends BaseObjectShould implements ObjectShould { - - public ProgramStageShould() { - super("program/program_stage.json"); - } - - @Override - @Test - public void map_from_json_string() throws IOException, ParseException { - ProgramStage programStage = objectMapper.readValue(jsonStream, ProgramStage.class); - - assertThat(programStage.lastUpdated()).isEqualTo( - BaseIdentifiableObject.DATE_FORMAT.parse("2013-04-10T12:15:02.041")); - assertThat(programStage.created()).isEqualTo( - BaseIdentifiableObject.DATE_FORMAT.parse("2013-03-04T11:41:07.541")); - - assertThat(programStage.uid()).isEqualTo("eaDHS084uMp"); - assertThat(programStage.name()).isEqualTo("ANC 1st visit"); - assertThat(programStage.displayName()).isEqualTo("ANC 1st visit"); - assertThat(programStage.description()).isEqualTo("ANC 1st visit"); - assertThat(programStage.displayDescription()).isEqualTo("ANC 1st visit"); - assertThat(programStage.sortOrder()).isEqualTo(1); - assertThat(programStage.allowGenerateNextVisit()).isFalse(); - assertThat(programStage.autoGenerateEvent()).isTrue(); - assertThat(programStage.blockEntryForm()).isFalse(); - assertThat(programStage.captureCoordinates()).isTrue(); - assertThat(programStage.displayGenerateEventBox()).isFalse(); - assertThat(programStage.executionDateLabel()).isNull(); - assertThat(programStage.dueDateLabel()).isEqualTo("Due date"); - assertThat(programStage.formType()).isEqualTo(FormType.DEFAULT); - assertThat(programStage.generatedByEnrollmentDate()).isFalse(); - assertThat(programStage.hideDueDate()).isFalse(); - assertThat(programStage.minDaysFromStart()).isEqualTo(0); - assertThat(programStage.openAfterEnrollment()).isFalse(); - assertThat(programStage.repeatable()).isFalse(); - assertThat(programStage.reportDateToUse()).isEqualTo("false"); - assertThat(programStage.standardInterval()).isNull(); - assertThat(ProgramStageInternalAccessor.accessProgramStageSections(programStage)).isEmpty(); - assertThat(programStage.periodType()).isEqualTo(PeriodType.Monthly); - assertThat(programStage.remindCompleted()).isFalse(); - assertThat(programStage.validationStrategy()).isEqualTo(ValidationStrategy.ON_UPDATE_AND_INSERT); - assertThat(programStage.featureType()).isEqualTo(FeatureType.POINT); - assertThat(programStage.enableUserAssignment()).isTrue(); - - List dataElements = - ProgramStageInternalAccessor.accessProgramStageDataElements(programStage); - assertThat(dataElements.get(0).uid()).isEqualTo("EQCf1l2Mdr8"); - assertThat(dataElements.get(1).uid()).isEqualTo("muxw4SGzUwJ"); - assertThat(dataElements.get(2).uid()).isEqualTo("KWybjio9UZT"); - assertThat(dataElements.get(3).uid()).isEqualTo("ejm0g2hwHHc"); - assertThat(dataElements.get(4).uid()).isEqualTo("yvV3txhSCyc"); - assertThat(dataElements.get(5).uid()).isEqualTo("fzQrjBpbwQD"); - assertThat(dataElements.get(6).uid()).isEqualTo("BbvkNf9PCxX"); - assertThat(dataElements.get(7).uid()).isEqualTo("MbdCfd4HaMQ"); - assertThat(dataElements.get(8).uid()).isEqualTo("SBn4XCFRbyT"); - assertThat(dataElements.get(9).uid()).isEqualTo("F0PZ4nZ86vo"); - assertThat(dataElements.get(10).uid()).isEqualTo("gFBRqVFh60H"); - assertThat(dataElements.get(11).uid()).isEqualTo("ljAoyjH4GYA"); - assertThat(dataElements.get(12).uid()).isEqualTo("MAdsNY2gOlv"); - assertThat(dataElements.get(13).uid()).isEqualTo("IpVUTCDdlGW"); - assertThat(dataElements.get(14).uid()).isEqualTo("psBtdqepNVM"); - assertThat(dataElements.get(15).uid()).isEqualTo("UzB6pZxZ2Rb"); - assertThat(dataElements.get(16).uid()).isEqualTo("FQZEMbBVabW"); - } -} diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt index a124c1def4..7ab9d63894 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramStageShould.kt @@ -71,6 +71,8 @@ class ProgramStageShould : BaseObjectShould("program/program_stage.json"), Objec assertThat(programStage.validationStrategy()).isEqualTo(ValidationStrategy.ON_UPDATE_AND_INSERT) assertThat(programStage.featureType()).isEqualTo(FeatureType.POINT) assertThat(programStage.enableUserAssignment()).isTrue() + assertThat(programStage.programStageLabel()).isEqualTo("ProgramStage Label") + assertThat(programStage.eventLabel()).isEqualTo("Event Label") val dataElements = ProgramStageInternalAccessor.accessProgramStageDataElements(programStage) assertThat(dataElements[0].uid()).isEqualTo("EQCf1l2Mdr8") From 5e2af0f6ddb02d56ceeced762c914d5ff7c7105f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 2 Feb 2024 14:16:04 +0100 Subject: [PATCH 045/222] [ANDROSDK-1806] Fix integration test --- .../dhis/android/core/data/program/ProgramSamples.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java index b4cf812edc..4ed5a88076 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java @@ -119,6 +119,13 @@ public static Program getAntenatalProgram() { .minAttributesRequiredToSearch(7) .featureType(FeatureType.NONE) .maxTeiCountToReturn(20) + .enrollmentLabel("Enrollment Label") + .followUpLabel("Follow up Label") + .orgUnitLabel("OrgUnit Label") + .relationshipLabel("Relationship Label") + .noteLabel("Note Label") + .trackedEntityAttributeLabel("TrackedEntityAttribute Label") + .build(); } From 6112450de13bda223f087e9baefe13158075bd73 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 6 Feb 2024 12:23:53 +0100 Subject: [PATCH 046/222] [ANDROSDK-1808] Add repository and service tests --- ...ckerLineListRepositoryIntegrationShould.kt | 11 +- .../trackerlinelist/TrackerLineListModel.kt | 28 +-- .../TrackerLineListRepository.kt | 2 +- .../internal/DataFilterHelper.kt | 6 +- .../internal/TrackerLineListRepositoryImpl.kt | 8 +- .../internal/TrackerLineListService.kt | 28 ++- .../TrackerLineListServiceMetadataHelper.kt | 159 +----------------- .../evaluator/ProgramAttributeEvaluator.kt | 27 ++- .../evaluator/TrackerLineListSQLLabel.kt | 2 +- .../internal/ProgramIndicatorSQLUtils.kt | 14 -- .../internal/dataitem/ProgramItemAttribute.kt | 2 +- .../dataitem/ProgramItemStageElement.kt | 2 +- .../internal/function/ProgramCountFunction.kt | 2 +- .../hisp/dhis/android/core/util/SqlUtils.kt | 47 ++++++ .../TrackerLineListRepositoryShould.kt | 76 +++++++++ 15 files changed, 190 insertions(+), 224 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/util/SqlUtils.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index 6fd4d34ef3..b076903c57 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -40,11 +40,14 @@ class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDi .withColumn( TrackerLineListItem.ProgramAttribute( uid = "cejWyOfXge6", - filters = listOf(DataFilter.GreaterThan("789")) - ) + filters = listOf( + DataFilter.GreaterThan("400000"), + DataFilter.LowerThan("700000"), + ), + ), ) .blockingEvaluate() - assertThat(result.getOrThrow().rows.size).isEqualTo(2) + assertThat(result.getOrThrow().rows.size).isEqualTo(1) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 14958b3bfe..8a6bb38782 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -57,33 +57,33 @@ sealed class TrackerLineListItem(val id: String) { val uid: String, val program: String, val programStage: String, - val filters: List + val filters: List, ) : TrackerLineListItem("$program.$programStage.$uid") - object CreatedBy: TrackerLineListItem(Label.CreatedBy) + object CreatedBy : TrackerLineListItem(Label.CreatedBy) - object LastUpdatedBy: TrackerLineListItem(Label.LastUpdatedBy) + object LastUpdatedBy : TrackerLineListItem(Label.LastUpdatedBy) - data class ProgramStatus(val filters: List): TrackerLineListItem(Label.ProgramStatus) + data class ProgramStatus(val filters: List) : TrackerLineListItem(Label.ProgramStatus) - data class EventStatus(val filters: List): TrackerLineListItem(Label.EventStatus) + data class EventStatus(val filters: List) : TrackerLineListItem(Label.EventStatus) } -sealed class DateFilter() { +sealed class DateFilter { data class Relative(val relative: RelativePeriod) : DateFilter() data class Absolute(val uid: String) : DateFilter() data class Range(val startDate: String, val endDate: String) : DateFilter() } -sealed class DataFilter() { - data class EqualTo(val value: String): DataFilter() - data class NotEqualTo(val value: String): DataFilter() - data class EqualToIgnoreCase(val value: String): DataFilter() - data class NotEqualToIgnoreCase(val value: String): DataFilter() +sealed class DataFilter { + data class EqualTo(val value: String) : DataFilter() + data class NotEqualTo(val value: String) : DataFilter() + data class EqualToIgnoreCase(val value: String) : DataFilter() + data class NotEqualToIgnoreCase(val value: String) : DataFilter() data class GreaterThan(val value: String) : DataFilter() data class GreaterThanOrEqualTo(val value: String) : DataFilter() - data class LessThan(val value: String) : DataFilter() - data class LessThanOrEqualTo(val value: String) : DataFilter() + data class LowerThan(val value: String) : DataFilter() + data class LowerThanOrEqualTo(val value: String) : DataFilter() data class Like(val value: String) : DataFilter() data class NotLike(val value: String) : DataFilter() data class LikeIgnoreCase(val value: String) : DataFilter() @@ -101,4 +101,4 @@ internal object Label { const val LastUpdatedBy = "lastUpdatedBy" const val ProgramStatus = "programStatus" const val EventStatus = "eventStatus" -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt index 8001bca19e..7becf593f2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -48,4 +48,4 @@ interface TrackerLineListRepository { fun evaluate(): Single> fun blockingEvaluate(): Result -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt index 4dcd7252c1..8c7dffe291 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt @@ -47,8 +47,8 @@ internal object DataFilterHelper { is DataFilter.NotEqualToIgnoreCase -> "!= '${filter.value}' COLLATE NOCASE" is DataFilter.GreaterThan -> "> ${filter.value}" is DataFilter.GreaterThanOrEqualTo -> ">= '${filter.value}'" - is DataFilter.LessThan -> "< '${filter.value}'" - is DataFilter.LessThanOrEqualTo -> "<= '${filter.value}'" + is DataFilter.LowerThan -> "< '${filter.value}'" + is DataFilter.LowerThanOrEqualTo -> "<= '${filter.value}'" is DataFilter.Like -> "= '%${filter.value}%'" is DataFilter.LikeIgnoreCase -> "= '%${filter.value}%' COLLATE NOCASE" is DataFilter.NotLike -> "!= '%${filter.value}%'" @@ -58,4 +58,4 @@ internal object DataFilterHelper { return "\"$itemId\" $comparison" } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index 2a87397ef3..5b1308fbc9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -47,7 +47,7 @@ internal class TrackerLineListRepositoryImpl( params.copy( outputType = TrackerLineListOutputType.EVENT, programId = programId, - programStageId = programStageId + programStageId = programStageId, ) } } @@ -56,7 +56,7 @@ internal class TrackerLineListRepositoryImpl( return updateParams { params.copy( outputType = TrackerLineListOutputType.ENROLLMENT, - programId = programId + programId = programId, ) } } @@ -83,7 +83,7 @@ internal class TrackerLineListRepositoryImpl( } private fun updateParams( - func: (params: TrackerLineListParams) -> TrackerLineListParams + func: (params: TrackerLineListParams) -> TrackerLineListParams, ): TrackerLineListRepositoryImpl { return TrackerLineListRepositoryImpl(func(params), service) } @@ -92,4 +92,4 @@ internal class TrackerLineListRepositoryImpl( val otherItems = items.filterNot { it.id == newItem.id } return otherItems + newItem } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index 36aea91ba7..1a19555dfb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -31,7 +31,6 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import android.database.Cursor import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem -import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper @@ -51,12 +50,11 @@ internal class TrackerLineListService( fun evaluate(params: TrackerLineListParams): Result { // TODO Validate params - // TODO Build metadata val metadata = metadataHelper.getMetadata(params) val sqlClause = when (params.outputType!!) { TrackerLineListOutputType.EVENT -> getEventSqlClause(params, metadata) - TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(params) + TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause() } val cursor = databaseAdapter.rawQuery(sqlClause) @@ -67,25 +65,25 @@ internal class TrackerLineListService( metadata = metadata, headers = emptyList(), filters = emptyList(), - rows = values - ) + rows = values, + ), ) } private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { return "SELECT " + - "${getEventSelectColumns(params, metadata)} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + - "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, metadata)} " + "${getEventSelectColumns(params, metadata)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, metadata)} " } - private fun getEnrollmentSqlClause(params: TrackerLineListParams): String { + private fun getEnrollmentSqlClause(): String { return TODO() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index dc254cd0f0..ac92a69e6c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -29,48 +29,15 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.AnalyticsException -import org.hisp.dhis.android.core.analytics.aggregated.DimensionItem import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem -import org.hisp.dhis.android.core.analytics.aggregated.internal.AnalyticsOrganisationUnitHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem -import org.hisp.dhis.android.core.category.internal.CategoryOptionComboStore -import org.hisp.dhis.android.core.category.internal.CategoryOptionStore -import org.hisp.dhis.android.core.category.internal.CategoryStore -import org.hisp.dhis.android.core.common.ObjectWithUid -import org.hisp.dhis.android.core.dataelement.DataElementOperand -import org.hisp.dhis.android.core.dataelement.internal.DataElementStore -import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemStore -import org.hisp.dhis.android.core.indicator.internal.IndicatorStore -import org.hisp.dhis.android.core.legendset.internal.LegendStore -import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitGroupStore -import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelStore -import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitStore -import org.hisp.dhis.android.core.period.internal.ParentPeriodGenerator -import org.hisp.dhis.android.core.period.internal.PeriodHelper -import org.hisp.dhis.android.core.program.ProgramIndicatorCollectionRepository -import org.hisp.dhis.android.core.program.internal.ProgramStore import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStore import org.koin.core.annotation.Singleton @Singleton @Suppress("LongParameterList") internal class TrackerLineListServiceMetadataHelper( - private val categoryStore: CategoryStore, - private val categoryOptionStore: CategoryOptionStore, - private val categoryOptionComboStore: CategoryOptionComboStore, - private val dataElementStore: DataElementStore, - private val indicatorStore: IndicatorStore, - private val expressionDimensionItemStore: ExpressionDimensionItemStore, - private val legendStore: LegendStore, - private val organisationUnitStore: OrganisationUnitStore, - private val organisationUnitGroupStore: OrganisationUnitGroupStore, - private val organisationUnitLevelStore: OrganisationUnitLevelStore, - private val programStore: ProgramStore, private val trackedEntityAttributeStore: TrackedEntityAttributeStore, - private val programIndicatorRepository: ProgramIndicatorCollectionRepository, - private val analyticsOrganisationUnitHelper: AnalyticsOrganisationUnitHelper, - private val parentPeriodGenerator: ParentPeriodGenerator, - private val periodHelper: PeriodHelper, ) { fun getMetadata(params: TrackerLineListParams): Map { @@ -104,131 +71,7 @@ internal class TrackerLineListServiceMetadataHelper( ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.uid) return listOf( - MetadataItem.TrackedEntityAttributeItem(attribute) - ) - } - - @SuppressWarnings("ThrowsCount", "ComplexMethod") - private fun getDataItems(item: DimensionItem.DataItem): List { - return listOf( - when (item) { - is DimensionItem.DataItem.DataElementItem -> - dataElementStore.selectByUid(item.uid) - ?.let { dataElement -> MetadataItem.DataElementItem(dataElement) } - ?: throw AnalyticsException.InvalidDataElement(item.uid) - - is DimensionItem.DataItem.DataElementOperandItem -> { - val dataElement = dataElementStore.selectByUid(item.dataElement) - val coc = categoryOptionComboStore.selectByUid(item.categoryOptionCombo) - if (dataElement == null || coc == null) { - throw AnalyticsException.InvalidDataElementOperand(item.id) - } - val dataElementOperand = DataElementOperand.builder() - .uid("${item.dataElement}.${item.categoryOptionCombo}") - .dataElement(ObjectWithUid.create(item.dataElement)) - .categoryOptionCombo(ObjectWithUid.create(item.categoryOptionCombo)) - .build() - - MetadataItem.DataElementOperandItem( - dataElementOperand, - dataElement.displayName()!!, - coc.displayName(), - ) - } - - is DimensionItem.DataItem.IndicatorItem -> - indicatorStore.selectByUid(item.uid) - ?.let { indicator -> MetadataItem.IndicatorItem(indicator) } - ?: throw AnalyticsException.InvalidIndicator(item.uid) - - is DimensionItem.DataItem.ProgramIndicatorItem -> - programIndicatorRepository.withAnalyticsPeriodBoundaries().uid(item.uid).blockingGet() - ?.let { programIndicator -> MetadataItem.ProgramIndicatorItem(programIndicator) } - ?: throw AnalyticsException.InvalidProgramIndicator(item.uid) - - is DimensionItem.DataItem.EventDataItem.DataElement -> { - val dataElement = dataElementStore.selectByUid(item.dataElement) - ?: throw AnalyticsException.InvalidDataElement(item.id) - val program = programStore.selectByUid(item.program) - ?: throw AnalyticsException.InvalidProgram(item.id) - - MetadataItem.EventDataElementItem(dataElement, program) - } - - is DimensionItem.DataItem.EventDataItem.Attribute -> { - val attribute = trackedEntityAttributeStore.selectByUid(item.attribute) - ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.id) - val program = programStore.selectByUid(item.program) - ?: throw AnalyticsException.InvalidProgram(item.id) - - MetadataItem.EventAttributeItem(attribute, program) - } - - is DimensionItem.DataItem.ExpressionDimensionItem -> { - val expressionItem = expressionDimensionItemStore.selectByUid(item.uid) - ?: throw AnalyticsException.InvalidExpressionDimensionItem(item.uid) - - MetadataItem.ExpressionDimensionItemItem(expressionItem) - } - }, - ) - } - - private fun getPeriodItems(item: DimensionItem.PeriodItem): List { - return listOf( - when (item) { - is DimensionItem.PeriodItem.Absolute -> { - val period = periodHelper.blockingGetPeriodForPeriodId(item.periodId) - MetadataItem.PeriodItem(period) - } - - is DimensionItem.PeriodItem.Relative -> { - val periods = parentPeriodGenerator.generateRelativePeriods(item.relative) - MetadataItem.RelativePeriodItem(item.relative, periods) - } - }, - ) - } - - @SuppressWarnings("ThrowsCount") - private fun getOrganisationUnitItems(item: DimensionItem.OrganisationUnitItem): List { - return listOf( - when (item) { - is DimensionItem.OrganisationUnitItem.Absolute -> - organisationUnitStore.selectByUid(item.uid) - ?.let { organisationUnit -> MetadataItem.OrganisationUnitItem(organisationUnit) } - ?: throw AnalyticsException.InvalidOrganisationUnit(item.uid) - - is DimensionItem.OrganisationUnitItem.Relative -> { - val ouUids = analyticsOrganisationUnitHelper.getRelativeOrganisationUnitUids(item.relative) - MetadataItem.OrganisationUnitRelativeItem(item.relative, ouUids) - } - - is DimensionItem.OrganisationUnitItem.Level -> { - organisationUnitLevelStore.selectByUid(item.uid)?.let { level -> - val ouUids = analyticsOrganisationUnitHelper.getOrganisationUnitUidsByLevel(level.level()!!) - MetadataItem.OrganisationUnitLevelItem(level, ouUids) - } ?: throw AnalyticsException.InvalidOrganisationUnitLevel(item.uid) - } - - is DimensionItem.OrganisationUnitItem.Group -> - organisationUnitGroupStore.selectByUid(item.uid)?.let { group -> - val ouUids = analyticsOrganisationUnitHelper.getOrganisationUnitUidsByGroup(item.uid) - MetadataItem.OrganisationUnitGroupItem(group, ouUids) - } ?: throw AnalyticsException.InvalidOrganisationUnitGroup(item.uid) - }, - ) - } - - private fun getCategoryItems(item: DimensionItem.CategoryItem): List { - return listOf( - categoryStore.selectByUid(item.uid) - ?.let { category -> MetadataItem.CategoryItem(category) } - ?: throw AnalyticsException.InvalidCategory(item.uid), - - categoryOptionStore.selectByUid(item.categoryOption) - ?.let { categoryOption -> MetadataItem.CategoryOptionItem(categoryOption) } - ?: throw AnalyticsException.InvalidCategoryOption(item.categoryOption), + MetadataItem.TrackedEntityAttributeItem(attribute), ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt index 2d6c3a153d..5cbb453613 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt @@ -28,24 +28,30 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator +import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo +import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast internal class ProgramAttributeEvaluator( private val item: TrackerLineListItem.ProgramAttribute, private val metadata: Map, ) : TrackerLineListEvaluator { override fun getSelectSQLForEvent(): String { - return "SELECT " + - "${TrackedEntityAttributeValueTableInfo.Columns.VALUE} " + - "FROM ${TrackedEntityAttributeValueTableInfo.TABLE_INFO.name()} " + - "WHERE ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_INSTANCE} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.TRACKED_ENTITY_INSTANCE} " + - "AND ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE} = '${item.id}'" + val column = getColumnValueCast( + column = TrackedEntityAttributeValueTableInfo.Columns.VALUE, + valueType = getAttribute().valueType(), + ) + return "SELECT $column " + + "FROM ${TrackedEntityAttributeValueTableInfo.TABLE_INFO.name()} " + + "WHERE ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_INSTANCE} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.TRACKED_ENTITY_INSTANCE} " + + "AND ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE} = '${item.id}'" } override fun getWhereSQLForEvent(): String { @@ -59,4 +65,11 @@ internal class ProgramAttributeEvaluator( override fun getWhereSQLForEnrollment(): String { TODO("Not yet implemented") } -} \ No newline at end of file + + private fun getAttribute(): TrackedEntityAttribute { + return ( + (metadata[item.id] ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.id)) as + MetadataItem.TrackedEntityAttributeItem + ).item + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt index 23f3f37b3d..322aec0ddf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt @@ -31,4 +31,4 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator object TrackerLineListSQLLabel { const val EventAlias = "ev" const val EnrollmentAlias = "en" -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/ProgramIndicatorSQLUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/ProgramIndicatorSQLUtils.kt index 1cec991795..5ec6b75b01 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/ProgramIndicatorSQLUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/ProgramIndicatorSQLUtils.kt @@ -127,20 +127,6 @@ internal object ProgramIndicatorSQLUtils { } } - fun getColumnValueCast( - column: String, - valueType: ValueType?, - ): String { - return when { - valueType?.isNumeric == true -> - "CAST($column AS NUMERIC)" - valueType?.isBoolean == true -> - "CASE WHEN $column = 'true' THEN 1 ELSE 0 END" - else -> - column - } - } - fun getDefaultValue( valueType: ValueType?, ): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemAttribute.kt b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemAttribute.kt index 60cf5aa9d6..d0230f2e01 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemAttribute.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemAttribute.kt @@ -33,10 +33,10 @@ import org.hisp.dhis.android.core.parser.internal.service.dataitem.DimensionalIt import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramExpressionItem import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorParserUtils.assumeProgramAttributeSyntax import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils -import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getColumnValueCast import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getDefaultValue import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo +import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast import org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext internal class ProgramItemAttribute : ProgramExpressionItem() { diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemStageElement.kt b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemStageElement.kt index af04e1ac7e..3a28391ef5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemStageElement.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/dataitem/ProgramItemStageElement.kt @@ -35,10 +35,10 @@ import org.hisp.dhis.android.core.parser.internal.service.dataitem.DimensionalIt import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramExpressionItem import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorParserUtils.assumeStageElementSyntax import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils -import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getColumnValueCast import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getDefaultValue import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValue import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo +import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast import org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext internal class ProgramItemStageElement : ProgramExpressionItem() { diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/function/ProgramCountFunction.kt b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/function/ProgramCountFunction.kt index 27b368b1bb..382db8ac99 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/function/ProgramCountFunction.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/programindicatorengine/internal/function/ProgramCountFunction.kt @@ -30,10 +30,10 @@ package org.hisp.dhis.android.core.program.programindicatorengine.internal.funct import org.hisp.dhis.android.core.event.EventTableInfo import org.hisp.dhis.android.core.parser.internal.expression.CommonExpressionVisitor import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramExpressionItem -import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getColumnValueCast import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils.getDataValueEventWhereClause import org.hisp.dhis.android.core.program.programindicatorengine.internal.dataitem.ProgramItemStageElement import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo +import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast import org.hisp.dhis.antlr.ParserExceptionWithoutContext import org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/SqlUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/util/SqlUtils.kt new file mode 100644 index 0000000000..a87825a924 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/util/SqlUtils.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +import org.hisp.dhis.android.core.common.ValueType + +internal object SqlUtils { + fun getColumnValueCast( + column: String, + valueType: ValueType?, + ): String { + return when { + valueType?.isNumeric == true -> + "CAST($column AS NUMERIC)" + valueType?.isBoolean == true -> + "CASE WHEN $column = 'true' THEN 1 ELSE 0 END" + else -> + column + } + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt new file mode 100644 index 0000000000..c518fe0b36 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import com.google.common.truth.Truth.assertThat +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListParams +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListRepositoryImpl +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListService +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class TrackerLineListRepositoryShould { + + private val service: TrackerLineListService = mock() + + private val initialParams = TrackerLineListParams(null, null, null, listOf(), listOf()) + + private val paramsCaptor = argumentCaptor() + + private val repository = TrackerLineListRepositoryImpl(initialParams, service) + + @Test + fun `Call service with overridden columns`() { + val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "program", "programStage", listOf()) + val de1_2 = de1_1.copy(filters = listOf(DataFilter.EqualTo("value"))) + val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "program", "programStage", listOf()) + + repository + .withColumn(de1_1) + .withColumn(de1_2) + .withColumn(de2_1) + .blockingEvaluate() + + verify(service).evaluate(paramsCaptor.capture()) + val columns = paramsCaptor.firstValue.columns + + assertThat(columns.size).isEqualTo(2) + + val dataElementColumns = columns.filterIsInstance() + val de1 = dataElementColumns.find { it.uid == "dataElement1" }!! + val de2 = dataElementColumns.find { it.uid == "dataElement2" }!! + + assertThat(de1.filters.size).isEqualTo(1) + assertThat(de2.filters.size).isEqualTo(0) + } +} From 75aba477e7990b3f9213d8d5f067010fb65dc491 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 6 Feb 2024 12:50:29 +0100 Subject: [PATCH 047/222] [ANDROSDK-1806-DEFECT] Add missing API fields --- .../dhis/android/core/program/internal/ProgramFields.kt | 6 ++++++ .../android/core/program/internal/ProgramStageFields.kt | 2 ++ 2 files changed, 8 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt index afbdd4484f..e8bbf07a1a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt @@ -95,5 +95,11 @@ internal object ProgramFields { fh.field(ProgramTableInfo.Columns.ACCESS_LEVEL), fh.nestedField(PROGRAM_SECTIONS).with(ProgramSectionFields.allFields), fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields), + fh.field(ProgramTableInfo.Columns.ENROLLMENT_LABEL), + fh.field(ProgramTableInfo.Columns.FOLLOW_UP_LABEL), + fh.field(ProgramTableInfo.Columns.ORG_UNIT_LABEL), + fh.field(ProgramTableInfo.Columns.RELATIONSHIP_LABEL), + fh.field(ProgramTableInfo.Columns.NOTE_LABEL), + fh.field(ProgramTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE_LABEL), ).build() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt index 852fccc03e..949b15c4fb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageFields.kt @@ -93,5 +93,7 @@ internal object ProgramStageFields { fh.field(ProgramStageTableInfo.Columns.VALIDATION_STRATEGY), fh.field(ProgramStageTableInfo.Columns.ENABLE_USER_ASSIGNMENT), fh.nestedField(ATTRIBUTE_VALUES).with(AttributeValuesFields.allFields), + fh.field(ProgramStageTableInfo.Columns.PROGRAM_STAGE_LABEL), + fh.field(ProgramStageTableInfo.Columns.EVENT_LABEL), ).build() } From 9d0b75f50340cca4be5c3b3dc2dde7e61cf9ed59 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 6 Feb 2024 13:51:07 +0100 Subject: [PATCH 048/222] [ANDROSDK-1810] Add TrackerVisualization model --- ...izationDimensionRepetitionColumnAdapter.kt | 51 +++++++ ...kerVisualizationOutputTypeColumnAdapter.kt | 36 +++++ .../TrackerVisualizationTypeColumnAdapter.kt | 36 +++++ .../ObjectWithUidListColumnAdapter.kt | 50 +++++++ ...sualizationDimensionListColumnAdapter.java | 37 +++++ .../visualization/TrackerVisualization.java | 134 ++++++++++++++++++ .../TrackerVisualizationDimension.java | 125 ++++++++++++++++ ...ackerVisualizationDimensionRepetition.java | 62 ++++++++ .../TrackerVisualizationOutputType.kt | 35 +++++ .../visualization/TrackerVisualizationType.kt | 49 +++++++ 10 files changed, 615 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt new file mode 100644 index 0000000000..7c1056dd75 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.custom.internal + +import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +internal class TrackerVisualizationDimensionRepetitionColumnAdapter : + JSONObjectColumnAdapter() { + + override fun getEnumClass(): Class { + return TrackerVisualizationDimensionRepetition::class.java + } + + override fun serialize(o: TrackerVisualizationDimensionRepetition?): String? { + return TrackerVisualizationDimensionRepetitionColumnAdapter.serialize(o) + } + + companion object { + fun serialize(o: TrackerVisualizationDimensionRepetition?): String? { + return o?.let { + ObjectMapperFactory.objectMapper().writeValueAsString(it) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt new file mode 100644 index 0000000000..2427c364ec --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType + +class TrackerVisualizationOutputTypeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return TrackerVisualizationOutputType::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt new file mode 100644 index 0000000000..9cde39b30d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType + +class TrackerVisualizationTypeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return TrackerVisualizationType::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt new file mode 100644 index 0000000000..d5e7f5aff9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal + +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.JSONObjectListColumnAdapter +import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory +import org.hisp.dhis.android.core.common.ObjectWithUid + +internal class ObjectWithUidListColumnAdapter : JSONObjectListColumnAdapter() { + override fun getObjectClass(): Class> { + return ArrayList().javaClass + } + + override fun serialize(o: List?): String? { + return ObjectWithUidListColumnAdapter.serialize(o) + } + + companion object { + fun serialize(o: List?): String? { + return o?.let { + ObjectMapperFactory.objectMapper().writeValueAsString(it) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java new file mode 100644 index 0000000000..b8abcc88e5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal; + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension; + +import java.util.List; + +public final class IgnoreTrackerVisualizationDimensionListColumnAdapter + extends IgnoreColumnAdapter> { +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java new file mode 100644 index 0000000000..d7be657254 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import android.database.Cursor; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.TrackerVisualizationOutputTypeColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.TrackerVisualizationTypeColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreTrackerVisualizationDimensionListColumnAdapter; +import org.hisp.dhis.android.core.common.BaseIdentifiableObject; +import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectWithUid; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = $$AutoValue_TrackerVisualization.Builder.class) +public abstract class TrackerVisualization extends BaseIdentifiableObject implements CoreObject { + + @Nullable + @JsonProperty() + public abstract String description(); + + @Nullable + @JsonProperty() + public abstract String displayDescription(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationTypeColumnAdapter.class) + public abstract TrackerVisualizationType type(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationOutputTypeColumnAdapter.class) + public abstract TrackerVisualizationOutputType outputType(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid program(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid programStage(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid trackedEntityType(); + + @Nullable + @JsonProperty() + @ColumnAdapter(IgnoreTrackerVisualizationDimensionListColumnAdapter.class) + public abstract List columns(); + + @Nullable + @JsonProperty() + @ColumnAdapter(IgnoreTrackerVisualizationDimensionListColumnAdapter.class) + public abstract List filters(); + + public static Builder builder() { + return new $$AutoValue_TrackerVisualization.Builder(); + } + + public static TrackerVisualization create(Cursor cursor) { + return AutoValue_TrackerVisualization.createFromCursor(cursor); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder extends BaseIdentifiableObject.Builder { + + public abstract Builder id(Long id); + + public abstract Builder description(String description); + + public abstract Builder displayDescription(String displayDescription); + + public abstract Builder type(TrackerVisualizationType type); + + public abstract Builder outputType(TrackerVisualizationOutputType type); + + public abstract Builder program(ObjectWithUid program); + + public abstract Builder programStage(ObjectWithUid programStage); + + public abstract Builder trackedEntityType(ObjectWithUid trackedEntityType); + + public abstract Builder columns(List columns); + + public abstract Builder filters(List filters); + + public abstract TrackerVisualization build(); + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java new file mode 100644 index 0000000000..f7641c6dbf --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.LayoutPositionColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidListColumnAdapter; +import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectWithUid; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = AutoValue_TrackerVisualizationDimension.Builder.class) +public abstract class TrackerVisualizationDimension implements CoreObject { + + @Nullable + public abstract String trackerVisualization(); + + @Nullable + @ColumnAdapter(LayoutPositionColumnAdapter.class) + public abstract LayoutPosition position(); + + @Nullable + @JsonProperty() + public abstract String dimension(); + + @Nullable + @JsonProperty() + public abstract String dimensionType(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid program(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid programStage(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidListColumnAdapter.class) + public abstract List items(); + + @Nullable + @JsonProperty() + public abstract String filter(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationDimensionRepetitionColumnAdapter.class) + public abstract TrackerVisualizationDimensionRepetition repetition(); + + + public static Builder builder() { + return new AutoValue_TrackerVisualizationDimension.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder id(Long id); + + public abstract Builder trackerVisualization(String trackerVisualization); + + public abstract Builder position(LayoutPosition position); + + public abstract Builder dimension(String dimension); + + public abstract Builder dimensionType(String dimensionType); + + public abstract Builder program(ObjectWithUid program); + + public abstract Builder programStage(ObjectWithUid programStage); + + public abstract Builder items(List items); + + public abstract Builder filter(String filter); + + public abstract Builder repetition(TrackerVisualizationDimensionRepetition repetition); + + public abstract TrackerVisualizationDimension build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java new file mode 100644 index 0000000000..7ca98f2e42 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = AutoValue_TrackerVisualizationDimensionRepetition.Builder.class) +public abstract class TrackerVisualizationDimensionRepetition { + + @Nullable + @JsonProperty() + public abstract List indexes(); + + public static Builder builder() { + return new AutoValue_TrackerVisualizationDimensionRepetition.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder indexes(List indexes); + + public abstract TrackerVisualizationDimensionRepetition build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt new file mode 100644 index 0000000000..ec0ed8f92d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +enum class TrackerVisualizationOutputType { + EVENT, + ENROLLMENT, + TRACKED_ENTITY_INSTANCE, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt new file mode 100644 index 0000000000..f4a3c7b431 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +enum class TrackerVisualizationType { + COLUMN, + STACKED_COLUMN, + BAR, + STACKED_BAR, + LINE, + LINE_LIST, + AREA, + STACKED_AREA, + PIE, + RADAR, + GAUGE, + YEAR_OVER_YEAR_LINE, + YEAR_OVER_YEAR_COLUMN, + SINGLE_VALUE, + PIVOT_TABLE, + SCATTER, + BUBBLE, +} From c5d9bfbd13c0026d3124f61d20c414695fd80c2c Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 12:30:45 +0100 Subject: [PATCH 049/222] [ANDROSDK-1810] Add TrackerVisualization store, handler and call --- .../MetadataCallMockIntegrationShould.kt | 4 +- ...lizationDimensionStoreIntegrationShould.kt | 58 ++++++++ ...ckerVisualizationStoreIntegrationShould.kt | 55 ++++++++ ...llectionRepositoryMockIntegrationShould.kt | 128 ++++++++++++++++++ core/src/main/assets/migrations/158.sql | 4 + core/src/main/assets/snapshots/snapshot.sql | 2 + .../arch/api/internal/ServicesDIModule.kt | 2 + .../access/internal/BaseDatabaseOpenHelper.kt | 2 +- .../ObjectWithUidListColumnAdapter.kt | 36 ++++- .../core/domain/metadata/MetadataCall.kt | 8 +- .../core/mockwebserver/Dhis2MockServer.java | 4 + ...rackerVisualizationCollectionRepository.kt | 100 ++++++++++++++ .../TrackerVisualizationDimension.java | 7 +- .../TrackerVisualizationDimensionTableInfo.kt | 86 ++++++++++++ .../TrackerVisualizationTableInfo.kt | 73 ++++++++++ .../core/visualization/VisualizationModule.kt | 2 + .../internal/TrackerVisualizationCall.kt | 79 +++++++++++ .../TrackerVisualizationCollectionCleaner.kt | 42 ++++++ ...alizationColumnsFiltersChildrenAppender.kt | 70 ++++++++++ .../TrackerVisualizationDimensionFields.kt | 54 ++++++++ .../TrackerVisualizationDimensionHandler.kt | 38 ++++++ ...rVisualizationDimensionRepetitionFields.kt | 45 ++++++ .../TrackerVisualizationDimensionStore.kt | 34 +++++ .../TrackerVisualizationDimensionStoreImpl.kt | 66 +++++++++ .../internal/TrackerVisualizationFields.kt | 62 +++++++++ .../internal/TrackerVisualizationHandler.kt | 68 ++++++++++ .../TrackerVisualizationModuleDownloader.kt | 45 ++++++ .../internal/TrackerVisualizationService.kt | 51 +++++++ .../internal/TrackerVisualizationStore.kt | 34 +++++ .../internal/TrackerVisualizationStoreImpl.kt | 65 +++++++++ .../internal/VisualizationModuleImpl.kt | 4 + .../internal/VisualizationModuleWiper.kt | 4 + .../TrackerVisualizationDimensionSamples.kt | 58 ++++++++ .../TrackerVisualizationSamples.kt | 73 ++++++++++ .../visualization/tracker_visualization.json | 74 ++++++++++ .../tracker_visualizations_1.json | 53 ++++++++ .../TrackerVisualizationShould.kt | 67 +++++++++ .../TrackerVisualizationHandlerShould.kt | 76 +++++++++++ 38 files changed, 1722 insertions(+), 11 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt create mode 100644 core/src/main/assets/migrations/158.sql create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt create mode 100644 core/src/sharedTest/resources/visualization/tracker_visualization.json create mode 100644 core/src/sharedTest/resources/visualization/tracker_visualizations_1.json create mode 100644 core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt index 09efa1d909..b05fe2076b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt @@ -45,6 +45,7 @@ import org.hisp.dhis.android.core.usecase.stock.StockUseCase import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestEmptyDispatcher import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.Visualization import org.junit.After import org.junit.Test @@ -64,7 +65,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher testObserver.awaitTerminalEvent() - testObserver.assertValueCount(16) + testObserver.assertValueCount(17) val values = testObserver.values() @@ -87,6 +88,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher DataSet::class, Category::class, Visualization::class, + TrackerVisualization::class, ProgramIndicator::class, Indicator::class, LegendSet::class, diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt new file mode 100644 index 0000000000..87c78a25c1 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.data.database.LinkStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.visualization.TrackerVisualizationDimensionSamples +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class TrackerVisualizationDimensionStoreIntegrationShould : + LinkStoreAbstractIntegrationShould( + TrackerVisualizationDimensionStoreImpl(TestDatabaseAdapterFactory.get()), + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), + ) { + override fun addMasterUid(): String { + return "tracker_visualization_uid" + } + + override fun buildObject(): TrackerVisualizationDimension { + return TrackerVisualizationDimensionSamples.trackerVisualizationDimension() + } + + override fun buildObjectWithOtherMasterUid(): TrackerVisualizationDimension { + return TrackerVisualizationDimensionSamples.trackerVisualizationDimension().toBuilder() + .trackerVisualization("tracker_visualization_uid_2") + .build() + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt new file mode 100644 index 0000000000..7319a62014 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.data.database.IdentifiableObjectStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.visualization.TrackerVisualizationSamples +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class TrackerVisualizationStoreIntegrationShould : + IdentifiableObjectStoreAbstractIntegrationShould( + TrackerVisualizationStoreImpl(TestDatabaseAdapterFactory.get()), + TrackerVisualizationTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), + ) { + override fun buildObject(): TrackerVisualization { + return TrackerVisualizationSamples.trackerVisualization() + } + + override fun buildObjectToUpdate(): TrackerVisualization { + return TrackerVisualizationSamples.trackerVisualization().toBuilder() + .type(TrackerVisualizationType.LINE) + .build() + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt new file mode 100644 index 0000000000..f20ba449cd --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.testapp.visualization + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType +import org.junit.Test + +class TrackerVisualizationCollectionRepositoryMockIntegrationShould : + BaseMockIntegrationTestFullDispatcher() { + @Test + fun find_all() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_uids() { + val visualizationUids = d2.visualizationModule().trackerVisualizations() + .blockingGetUids() + + assertThat(visualizationUids.size).isEqualTo(1) + assertThat(visualizationUids.contains("s85urBIkN0z")).isTrue() + } + + @Test + fun find_by_description() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byDescription().eq("Child line list description") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_display_description() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byDisplayDescription().eq("Child line list description") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_type() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byType().eq(TrackerVisualizationType.LINE_LIST) + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_output_type() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byOutputType().eq(TrackerVisualizationOutputType.ENROLLMENT) + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_program() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byProgram().eq("IpHINAT79UW") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_program_stage() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byProgramStage().eq("IpHINAT79UW") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(0) + } + + @Test + fun include_columns_and_filters_as_children() { + val visualization = d2.visualizationModule().trackerVisualizations() + .withColumnsAndFilters() + .uid("s85urBIkN0z") + .blockingGet()!! + + assertThat(visualization.columns()!!.size).isEqualTo(3) + assertThat(visualization.columns()!![0].dimension()).isEqualTo("ou") + assertThat(visualization.columns()!![0].dimensionType()).isEqualTo("ORGANISATION_UNIT") + assertThat(visualization.columns()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.columns()!![0].items()!![0].uid()).isEqualTo("USER_ORGUNIT") + + assertThat(visualization.filters()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].dimension()).isEqualTo("enrollmentDate") + assertThat(visualization.filters()!![0].dimensionType()).isEqualTo("PERIOD") + assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("LAST_10_YEARS") + } +} diff --git a/core/src/main/assets/migrations/158.sql b/core/src/main/assets/migrations/158.sql new file mode 100644 index 0000000000..cd9e8c00d5 --- /dev/null +++ b/core/src/main/assets/migrations/158.sql @@ -0,0 +1,4 @@ +# Add TrackerVisualization model (ANDROSDK-1810) + +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index f6fdcef9de..03203473e7 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -130,3 +130,5 @@ CREATE TABLE DataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NO CREATE TABLE ProgramStageWorkingList (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, eventStatus TEXT, eventCreatedAt TEXT, eventOccurredAt TEXT, eventScheduledAt TEXT, enrollmentStatus TEXT, enrolledAt TEXT, enrollmentOccurredAt TEXT, orderProperty TEXT, displayColumnOrder TEXT, orgUnit TEXT, ouMode TEXT, assignedUserMode TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (orgUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadURL TEXT, version TEXT); CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt index 019ce788df..88afbf8e25 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt @@ -47,6 +47,7 @@ import org.hisp.dhis.android.core.usecase.stock.internal.StockUseCaseService import org.hisp.dhis.android.core.user.internal.AuthorityService import org.hisp.dhis.android.core.user.internal.UserService import org.hisp.dhis.android.core.validation.internal.ValidationRuleService +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationService import org.hisp.dhis.android.core.visualization.internal.VisualizationService import org.koin.dsl.module import retrofit2.Retrofit @@ -97,6 +98,7 @@ internal val servicesDIModule = module { single { get().create(TrackedEntityTypeService::class.java) } single { get().create(TrackerExporterService::class.java) } single { get().create(TrackerImporterService::class.java) } + single { get().create(TrackerVisualizationService::class.java) } single { get().create(UserService::class.java) } single { get().create(ValidationRuleService::class.java) } single { get().create(VisualizationService::class.java) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index 6226e4b1fd..604c32b648 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 157 + const val VERSION = 158 } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt index d5e7f5aff9..b2737fcb56 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt @@ -27,22 +27,44 @@ */ package org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal -import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.JSONObjectListColumnAdapter +import android.content.ContentValues +import android.database.Cursor +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.JsonMappingException +import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory import org.hisp.dhis.android.core.common.ObjectWithUid -internal class ObjectWithUidListColumnAdapter : JSONObjectListColumnAdapter() { - override fun getObjectClass(): Class> { - return ArrayList().javaClass +internal class ObjectWithUidListColumnAdapter : ColumnTypeAdapter> { + + override fun fromCursor(cursor: Cursor, columnName: String): List { + val columnIndex = cursor.getColumnIndex(columnName) + val str = cursor.getString(columnIndex) + return try { + val idList = ObjectMapperFactory.objectMapper().readValue(str, ArrayList().javaClass) + idList.map { ObjectWithUid.create(it) } + } catch (e: JsonProcessingException) { + listOf() + } catch (e: JsonMappingException) { + listOf() + } catch (e: IllegalArgumentException) { + listOf() + } catch (e: IllegalStateException) { + listOf() + } } - override fun serialize(o: List?): String? { - return ObjectWithUidListColumnAdapter.serialize(o) + override fun toContentValues(values: ContentValues, columnName: String, value: List?) { + try { + values.put(columnName, serialize(value)) + } catch (e: JsonProcessingException) { + e.printStackTrace() + } } companion object { fun serialize(o: List?): String? { - return o?.let { + return o?.map { it.uid() }.let { ObjectMapperFactory.objectMapper().writeValueAsString(it) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt index ebb68b02e7..a24e4cb19e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt @@ -69,7 +69,9 @@ import org.hisp.dhis.android.core.usecase.UseCaseModuleDownloader import org.hisp.dhis.android.core.usecase.stock.StockUseCase import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.internal.UserModuleDownloader +import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.Visualization +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationModuleDownloader import org.hisp.dhis.android.core.visualization.internal.VisualizationModuleDownloader import org.koin.core.annotation.Singleton @@ -86,6 +88,7 @@ internal class MetadataCall( private val organisationUnitModuleDownloader: OrganisationUnitModuleDownloader, private val dataSetDownloader: DataSetModuleDownloader, private val visualizationDownloader: VisualizationModuleDownloader, + private val trackerVisualizationDownloader: TrackerVisualizationModuleDownloader, private val constantModuleDownloader: ConstantModuleDownloader, private val indicatorModuleDownloader: IndicatorModuleDownloader, private val programIndicatorModuleDownloader: ProgramIndicatorModuleDownloader, @@ -100,7 +103,7 @@ internal class MetadataCall( ) { companion object { - const val CALLS_COUNT = 15 + const val CALLS_COUNT = 16 } @Suppress("TooGenericExceptionCaught") @@ -159,6 +162,9 @@ internal class MetadataCall( visualizationDownloader.downloadMetadata() emit(progressManager.increaseProgress(Visualization::class.java, false)) + trackerVisualizationDownloader.downloadMetadata() + emit(progressManager.increaseProgress(TrackerVisualization::class.java, false)) + programIndicatorModuleDownloader.downloadMetadata() emit(progressManager.increaseProgress(ProgramIndicator::class.java, false)) diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index b86b31288b..62e48eff2f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -95,6 +95,7 @@ public class Dhis2MockServer { private static final String CATEGORY_OPTION_ORGUNITS_JSON = "category/category_option_orgunits.json"; private static final String VISUALIZATIONS_1_JSON = "visualization/visualizations_1.json"; private static final String VISUALIZATIONS_2_JSON = "visualization/visualizations_2.json"; + private static final String TRACKER_VISUALIZATIONS_1_JSON = "visualization/tracker_visualizations_1.json"; private static final String ORGANISATION_UNIT_LEVELS_JSON = "organisationunit/organisation_unit_levels.json"; private static final String CONSTANTS_JSON = "constant/constants.json"; private static final String USER_JSON = "user/user38.json"; @@ -274,6 +275,8 @@ public MockResponse dispatch(RecordedRequest request) { return createMockResponse(VISUALIZATIONS_1_JSON); } else if (path.startsWith("/api/visualizations/FAFa11yFeFe?")) { return createMockResponse(VISUALIZATIONS_2_JSON); + } else if (path.startsWith("/api/eventVisualizations/s85urBIkN0z?")) { + return createMockResponse(TRACKER_VISUALIZATIONS_1_JSON); } else if (path.startsWith("/api/organisationUnits?")) { return createMockResponse(ORGANISATION_UNITS_JSON); } else if (path.startsWith("/api/organisationUnitLevels?")) { @@ -374,6 +377,7 @@ public void enqueueMetadataResponses() { enqueueMockResponse(CATEGORY_OPTION_ORGUNITS_JSON); enqueueMockResponse(VISUALIZATIONS_1_JSON); enqueueMockResponse(VISUALIZATIONS_2_JSON); + enqueueMockResponse(TRACKER_VISUALIZATIONS_1_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); enqueueMockResponse(INDICATORS_JSON); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt new file mode 100644 index 0000000000..21fa8c6bc6 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.arch.repositories.collection.internal.ReadOnlyIdentifiableCollectionRepositoryImpl +import org.hisp.dhis.android.core.arch.repositories.filters.internal.EnumFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.FilterConnectorFactory +import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationColumnsFiltersChildrenAppender +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationFields +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationStore +import org.hisp.dhis.android.core.visualization.internal.VisualizationFields +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("TooManyFunctions") +class TrackerVisualizationCollectionRepository internal constructor( + store: TrackerVisualizationStore, + databaseAdapter: DatabaseAdapter, + scope: RepositoryScope, +) : ReadOnlyIdentifiableCollectionRepositoryImpl( + store, + databaseAdapter, + childrenAppenders, + scope, + FilterConnectorFactory( + scope, + ) { s: RepositoryScope -> + TrackerVisualizationCollectionRepository( + store, + databaseAdapter, + s, + ) + }, +) { + fun byDescription(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.DESCRIPTION) + } + + fun byDisplayDescription(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.DISPLAY_DESCRIPTION) + } + + fun byType(): EnumFilterConnector { + return cf.enumC(TrackerVisualizationTableInfo.Columns.TYPE) + } + + fun byOutputType(): EnumFilterConnector { + return cf.enumC(TrackerVisualizationTableInfo.Columns.OUTPUT_TYPE) + } + + fun byProgram(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.PROGRAM) + } + + fun byProgramStage(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.PROGRAM_STAGE) + } + + fun byTrackedEntityType(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.TRACKED_ENTITY_TYPE) + } + fun withColumnsAndFilters(): TrackerVisualizationCollectionRepository { + return cf.withChild(VisualizationFields.ITEMS) + } + + internal companion object { + val childrenAppenders: ChildrenAppenderGetter = mapOf( + TrackerVisualizationFields.ITEMS to TrackerVisualizationColumnsFiltersChildrenAppender::create, + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java index f7641c6dbf..7557ca128b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java @@ -28,6 +28,8 @@ package org.hisp.dhis.android.core.visualization; +import android.database.Cursor; + import androidx.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonProperty; @@ -36,7 +38,6 @@ import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; -import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.LayoutPositionColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; @@ -94,6 +95,10 @@ public static Builder builder() { return new AutoValue_TrackerVisualizationDimension.Builder(); } + public static TrackerVisualizationDimension create(Cursor cursor) { + return $AutoValue_TrackerVisualizationDimension.createFromCursor(cursor); + } + public abstract Builder toBuilder(); @AutoValue.Builder diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt new file mode 100644 index 0000000000..f1172a1261 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns + +object TrackerVisualizationDimensionTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "TrackerVisualizationDimension" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : CoreColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + TRACKER_VISUALIZATION, + POSITION, + DIMENSION, + DIMENSION_TYPE, + PROGRAM, + PROGRAM_STAGE, + ITEMS, + FILTER, + REPETITION, + ) + } + + override fun whereUpdate(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + TRACKER_VISUALIZATION, + POSITION, + DIMENSION, + DIMENSION_TYPE, + ) + } + + companion object { + const val TRACKER_VISUALIZATION = "trackerVisualization" + const val POSITION = "position" + const val DIMENSION = "dimension" + const val DIMENSION_TYPE = "dimensionType" + const val PROGRAM = "program" + const val PROGRAM_STAGE = "programStage" + const val ITEMS = "items" + const val FILTER = "filter" + const val REPETITION = "repetition" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt new file mode 100644 index 0000000000..f80ffef08b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns +import org.hisp.dhis.android.core.common.IdentifiableColumns + +object TrackerVisualizationTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "TrackerVisualization" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : IdentifiableColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + DESCRIPTION, + DISPLAY_DESCRIPTION, + TYPE, + OUTPUT_TYPE, + PROGRAM, + PROGRAM_STAGE, + TRACKED_ENTITY_TYPE, + ) + } + + companion object { + const val DESCRIPTION = "description" + const val DISPLAY_DESCRIPTION = "displayDescription" + const val TYPE = "type" + const val OUTPUT_TYPE = "outputType" + const val PROGRAM = "program" + const val PROGRAM_STAGE = "programStage" + const val TRACKED_ENTITY_TYPE = "trackedEntityType" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt index 06627c886b..1a70429432 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt @@ -29,4 +29,6 @@ package org.hisp.dhis.android.core.visualization interface VisualizationModule { fun visualizations(): VisualizationCollectionRepository + + fun trackerVisualizations(): TrackerVisualizationCollectionRepository } diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt new file mode 100644 index 0000000000..ae4855d602 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.arch.call.factories.internal.UidsCallCoroutines +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.systeminfo.DHISVersion +import org.hisp.dhis.android.core.systeminfo.DHISVersionManager +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationCall( + private val handler: TrackerVisualizationHandler, + private val service: TrackerVisualizationService, + private val dhis2VersionManager: DHISVersionManager, + private val apiDownloader: APIDownloader, +) : UidsCallCoroutines { + + companion object { + // Workaround for DHIS2-16746. Force queries to entity endpoint instead of list endpoint. + private const val MAX_UID_LIST_SIZE = 1 + } + + override suspend fun download(uids: Set): List { + val accessFilter = "access." + AccessFields.read.eq(true).generateString() + + // TODO Limit by version + + return if (dhis2VersionManager.isGreaterOrEqualThan(DHISVersion.V2_38)) { + apiDownloader.downloadPartitioned( + uids, + MAX_UID_LIST_SIZE, + handler, + ) { partitionUids: Set -> + try { + val visualization = service.getSingleTrackerVisualization( + partitionUids.first(), + TrackerVisualizationFields.allFields, + accessFilter = accessFilter, + paging = false, + ) + Payload(listOf(visualization)) + } catch (ignored: Exception) { + Payload() + } + } + } else { + emptyList() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt new file mode 100644 index 0000000000..f9ce80f3c6 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.cleaners.internal.CollectionCleanerImpl +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationCollectionCleaner( + databaseAdapter: DatabaseAdapter, +) : CollectionCleanerImpl( + tableName = TrackerVisualizationTableInfo.TABLE_INFO.name(), + databaseAdapter = databaseAdapter, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt new file mode 100644 index 0000000000..f9b0590e61 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.internal.SingleParentChildStore +import org.hisp.dhis.android.core.arch.db.stores.internal.StoreFactory.singleParentChildStore +import org.hisp.dhis.android.core.arch.db.stores.projections.internal.SingleParentChildProjection +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppender +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo + +internal class TrackerVisualizationColumnsFiltersChildrenAppender private constructor( + private val linkChildStore: SingleParentChildStore, +) : ChildrenAppender() { + override fun appendChildren(m: TrackerVisualization): TrackerVisualization { + val items = linkChildStore.getChildren(m) + val groupedByPosition = items + .groupBy { it.position() } + + return m.toBuilder() + .columns(groupedByPosition[LayoutPosition.COLUMN] ?: emptyList()) + .filters(groupedByPosition[LayoutPosition.FILTER] ?: emptyList()) + .build() + } + + companion object { + private val CHILD_PROJECTION = SingleParentChildProjection( + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.Columns.TRACKER_VISUALIZATION, + ) + + fun create(databaseAdapter: DatabaseAdapter): ChildrenAppender { + return TrackerVisualizationColumnsFiltersChildrenAppender( + singleParentChildStore( + databaseAdapter, + CHILD_PROJECTION, + ) { cursor: Cursor -> TrackerVisualizationDimension.create(cursor) }, + ) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt new file mode 100644 index 0000000000..512eca9515 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo + +internal object TrackerVisualizationDimensionFields { + private val fh = FieldsHelper() + + val allFields: Fields = + Fields.builder() + .fields( + fh.field(TrackerVisualizationDimensionTableInfo.Columns.DIMENSION), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.DIMENSION_TYPE), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.PROGRAM), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.PROGRAM_STAGE), + fh.nestedFieldWithUid(TrackerVisualizationDimensionTableInfo.Columns.ITEMS), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.FILTER), + fh.nestedField( + TrackerVisualizationDimensionTableInfo.Columns.REPETITION, + ) + .with(TrackerVisualizationDimensionRepetitionFields.allFields), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt new file mode 100644 index 0000000000..cc8d979962 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.LinkHandlerImpl +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationDimensionHandler( + store: TrackerVisualizationDimensionStore, +) : LinkHandlerImpl(store) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt new file mode 100644 index 0000000000..e1cd496319 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +internal object TrackerVisualizationDimensionRepetitionFields { + private const val INDEXES = "indexes" + + private val fh = FieldsHelper() + + val allFields: Fields = + Fields.builder() + .fields( + fh.field(INDEXES), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt new file mode 100644 index 0000000000..3f3ae075b2 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.LinkStore +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension + +internal interface TrackerVisualizationDimensionStore : LinkStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt new file mode 100644 index 0000000000..d3a329a089 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidListColumnAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper +import org.hisp.dhis.android.core.arch.db.stores.internal.LinkStoreImpl +import org.hisp.dhis.android.core.arch.helpers.UidsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("MagicNumber") +internal class TrackerVisualizationDimensionStoreImpl( + databaseAdapter: DatabaseAdapter, +) : TrackerVisualizationDimensionStore, + LinkStoreImpl( + databaseAdapter, + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.Columns.TRACKER_VISUALIZATION, + BINDER, + { TrackerVisualizationDimension.create(it) }, + ) { + companion object { + private val BINDER = StatementBinder { o: TrackerVisualizationDimension, w: StatementWrapper -> + w.bind(1, o.trackerVisualization()) + w.bind(2, o.position()) + w.bind(3, o.dimension()) + w.bind(4, o.dimensionType()) + w.bind(5, UidsHelper.getUidOrNull(o.program())) + w.bind(6, UidsHelper.getUidOrNull(o.programStage())) + w.bind(7, ObjectWithUidListColumnAdapter.serialize(o.items())) + w.bind(8, o.filter()) + w.bind(9, TrackerVisualizationDimensionRepetitionColumnAdapter.serialize(o.repetition())) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt new file mode 100644 index 0000000000..c66c6a55b5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo + +internal object TrackerVisualizationFields { + private const val COLUMNS = "columns" + private const val FILTERS = "filters" + + internal const val ITEMS = "items" + + private val fh = FieldsHelper() + val uid = fh.uid() + + val allFields: Fields = + Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(TrackerVisualizationTableInfo.Columns.DESCRIPTION), + fh.field(TrackerVisualizationTableInfo.Columns.DISPLAY_DESCRIPTION), + fh.field(TrackerVisualizationTableInfo.Columns.TYPE), + fh.field(TrackerVisualizationTableInfo.Columns.OUTPUT_TYPE), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.PROGRAM), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.PROGRAM_STAGE), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.TRACKED_ENTITY_TYPE), + fh.nestedField(COLUMNS) + .with(TrackerVisualizationDimensionFields.allFields), + fh.nestedField(FILTERS) + .with(TrackerVisualizationDimensionFields.allFields), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt new file mode 100644 index 0000000000..ad25800f2c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationHandler( + store: TrackerVisualizationStore, + private val trackerVisualizationCollectionCleaner: TrackerVisualizationCollectionCleaner, + private val dimensionHandler: TrackerVisualizationDimensionHandler, +) : IdentifiableHandlerImpl(store) { + + override fun afterObjectHandled(o: TrackerVisualization, action: HandleAction) { + val dimensions = + toDimensions(o.columns(), LayoutPosition.COLUMN) + + toDimensions(o.filters(), LayoutPosition.FILTER) + + dimensionHandler.handleMany(o.uid(), dimensions) { + it.toBuilder().trackerVisualization(o.uid()).build() + } + } + + override fun afterCollectionHandled(oCollection: Collection?) { + trackerVisualizationCollectionCleaner.deleteNotPresent(oCollection) + } + + private fun toDimensions( + dimensions: List?, + position: LayoutPosition, + ): List { + return dimensions?.map { dimension -> + dimension.toBuilder() + .position(position) + .build() + } ?: emptyList() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt new file mode 100644 index 0000000000..f419ddfc09 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.modules.internal.TypedModuleDownloader +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationModuleDownloader internal constructor( + private val visualizationCall: TrackerVisualizationCall, +) : + TypedModuleDownloader> { + + override suspend fun downloadMetadata(): List { + // TODO Extract tracker visualization uids from analytics settings + val trackerVisualizations = setOf("s85urBIkN0z") + return visualizationCall.download(trackerVisualizations) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt new file mode 100644 index 0000000000..72caa546f0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface TrackerVisualizationService { + + @GET("$TRACKER_VISUALIZATIONS/{$TRACKER_VISUALIZATION_UID}") + suspend fun getSingleTrackerVisualization( + @Path(TRACKER_VISUALIZATION_UID) uid: String, + @Query("fields") @Which fields: Fields, + @Query("filter") accessFilter: String, + @Query("paging") paging: Boolean, + ): TrackerVisualization + + companion object { + const val TRACKER_VISUALIZATIONS = "eventVisualizations" + const val TRACKER_VISUALIZATION_UID = "visualizationUid" + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt new file mode 100644 index 0000000000..faa084da03 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore +import org.hisp.dhis.android.core.visualization.TrackerVisualization + +internal interface TrackerVisualizationStore : IdentifiableObjectStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt new file mode 100644 index 0000000000..a0dca332db --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.IdentifiableStatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStoreImpl +import org.hisp.dhis.android.core.arch.helpers.UidsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("MagicNumber") +internal class TrackerVisualizationStoreImpl( + databaseAdapter: DatabaseAdapter, +) : TrackerVisualizationStore, + IdentifiableObjectStoreImpl( + databaseAdapter, + TrackerVisualizationTableInfo.TABLE_INFO, + BINDER, + { cursor: Cursor -> TrackerVisualization.create(cursor) }, + ) { + companion object { + private val BINDER = object : IdentifiableStatementBinder() { + override fun bindToStatement(o: TrackerVisualization, w: StatementWrapper) { + super.bindToStatement(o, w) + w.bind(7, o.description()) + w.bind(8, o.displayDescription()) + w.bind(9, o.type()) + w.bind(10, o.outputType()) + w.bind(11, UidsHelper.getUidOrNull(o.program())) + w.bind(12, UidsHelper.getUidOrNull(o.programStage())) + w.bind(13, UidsHelper.getUidOrNull(o.trackedEntityType())) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt index 6971804aef..470ac76ea6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt @@ -27,6 +27,7 @@ */ package org.hisp.dhis.android.core.visualization.internal +import org.hisp.dhis.android.core.visualization.TrackerVisualizationCollectionRepository import org.hisp.dhis.android.core.visualization.VisualizationCollectionRepository import org.hisp.dhis.android.core.visualization.VisualizationModule import org.koin.core.annotation.Singleton @@ -34,7 +35,10 @@ import org.koin.core.annotation.Singleton @Singleton internal class VisualizationModuleImpl( private val visualizations: VisualizationCollectionRepository, + private val trackerVisualizations: TrackerVisualizationCollectionRepository, ) : VisualizationModule { override fun visualizations(): VisualizationCollectionRepository = visualizations + + override fun trackerVisualizations(): TrackerVisualizationCollectionRepository = trackerVisualizations } diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt index c4f3801fed..79df93bd42 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt @@ -27,6 +27,8 @@ */ package org.hisp.dhis.android.core.visualization.internal +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo import org.hisp.dhis.android.core.visualization.VisualizationDimensionItemTableInfo import org.hisp.dhis.android.core.visualization.VisualizationTableInfo import org.hisp.dhis.android.core.wipe.internal.ModuleWiper @@ -37,6 +39,8 @@ import org.koin.core.annotation.Singleton class VisualizationModuleWiper internal constructor(private val tableWiper: TableWiper) : ModuleWiper { override fun wipeMetadata() { tableWiper.wipeTables( + TrackerVisualizationTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.TABLE_INFO, VisualizationTableInfo.TABLE_INFO, VisualizationDimensionItemTableInfo.TABLE_INFO, ) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt new file mode 100644 index 0000000000..a402efdd7c --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.data.visualization + +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +object TrackerVisualizationDimensionSamples { + + fun trackerVisualizationDimension(): TrackerVisualizationDimension = + TrackerVisualizationDimension.builder() + .id(1L) + .trackerVisualization("tracker_visualization_uid") + .position(LayoutPosition.COLUMN) + .dimension("ou") + .dimensionType("ORGANISATION_UNIT") + .program(ObjectWithUid.create("program_uid")) + .programStage(ObjectWithUid.create("program_stage_uid")) + .items( + listOf( + ObjectWithUid.create("USER_ORGUNIT"), + ObjectWithUid.create("USER_ORGUNIT_CHILDREN"), + ), + ) + .repetition( + TrackerVisualizationDimensionRepetition.builder() + .indexes(listOf(-1, 1, 0)) + .build(), + ) + .build() +} diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt new file mode 100644 index 0000000000..9f9e50206e --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.data.visualization + +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.data.utils.FillPropertiesTestUtils +import org.hisp.dhis.android.core.visualization.* + +internal object TrackerVisualizationSamples { + + private const val DATE_STR = "2021-06-16T14:26:50.195" + private val DATE = FillPropertiesTestUtils.parseDate(DATE_STR) + + @JvmStatic + fun trackerVisualization(): TrackerVisualization = TrackerVisualization.builder() + .id(1L) + .uid("PYBH8ZaAQnC") + .name("Android SDK Tracker Visualization sample") + .displayName("Android SDK Tracker Visualization sample") + .created(DATE) + .lastUpdated(DATE) + .description("Sample tracker visualization for the Android SDK") + .displayDescription("Sample tracker visualization for the Android SDK") + .type(TrackerVisualizationType.LINE_LIST) + .outputType(TrackerVisualizationOutputType.ENROLLMENT) + .program(ObjectWithUid.create("")) + .programStage(ObjectWithUid.create("")) + .trackedEntityType(ObjectWithUid.create("")) + .columns( + listOf( + TrackerVisualizationDimension.builder() + .dimension("ou") + .dimensionType("ORGANISATION_UNIT") + .items(listOf(ObjectWithUid.create("USER_ORGUNIT"))) + .build(), + ), + ) + .filters( + listOf( + TrackerVisualizationDimension.builder() + .dimension("enrollmentDate") + .dimensionType("PERIOD") + .items(listOf(ObjectWithUid.create("LAST_5_YEARS"))) + .build(), + ), + ) + .build() +} diff --git a/core/src/sharedTest/resources/visualization/tracker_visualization.json b/core/src/sharedTest/resources/visualization/tracker_visualization.json new file mode 100644 index 0000000000..8593ea5dba --- /dev/null +++ b/core/src/sharedTest/resources/visualization/tracker_visualization.json @@ -0,0 +1,74 @@ +{ + "name": "TB program", + "created": "2024-02-07T07:37:41.116", + "lastUpdated": "2024-02-07T07:43:25.556", + "description": "Line list for TB program", + "type": "LINE_LIST", + "program": { + "id": "ur1Edk5Oe2n" + }, + "outputType": "ENROLLMENT", + "filters": [ + { + "dimensionType": "PERIOD", + "items": [ + { + "id": "LAST_5_YEARS" + } + ], + "dimension": "enrollmentDate" + } + ], + "columns": [ + { + "dimensionType": "ORGANISATION_UNIT", + "items": [ + { + "id": "USER_ORGUNIT" + } + ], + "dimension": "ou" + }, + { + "dimensionType": "PROGRAM_ATTRIBUTE", + "items": [], + "dimension": "w75KJ2mc4zz" + }, + { + "dimensionType": "DATA_X", + "items": [ + { + "id": "COMPLETED" + }, + { + "id": "ACTIVE" + } + ], + "dimension": "programStatus" + }, + { + "dimensionType": "PROGRAM_DATA_ELEMENT", + "items": [], + "programStage": { + "id": "EPEcjy3FWmI" + }, + "program": { + "id": "ur1Edk5Oe2n" + }, + "filter": "IN:1", + "dimension": "fCXKBdc27Bt", + "repetition": { + "indexes": [ + 1, + 2, + -2, + -1, + 0 + ] + } + } + ], + "displayName": "TB program", + "displayDescription": "Line list for TB program", + "id": "s85urBIkN0z" +} diff --git a/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json new file mode 100644 index 0000000000..9b0898d440 --- /dev/null +++ b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json @@ -0,0 +1,53 @@ +{ + "name": "Child line list", + "created": "2024-02-07T07:37:41.116", + "lastUpdated": "2024-02-07T07:43:25.556", + "description": "Child line list description", + "type": "LINE_LIST", + "program": { + "id": "IpHINAT79UW" + }, + "outputType": "ENROLLMENT", + "filters": [ + { + "dimensionType": "PERIOD", + "items": [ + { + "id": "LAST_10_YEARS" + } + ], + "dimension": "enrollmentDate" + } + ], + "columns": [ + { + "dimensionType": "ORGANISATION_UNIT", + "items": [ + { + "id": "USER_ORGUNIT" + } + ], + "dimension": "ou" + }, + { + "dimensionType": "PROGRAM_ATTRIBUTE", + "items": [], + "dimension": "cejWyOfXge6" + }, + { + "dimensionType": "DATA_X", + "items": [ + { + "id": "COMPLETED" + }, + { + "id": "ACTIVE" + } + ], + "dimension": "programStatus" + } + ], + "displayName": "Child line list", + "displayDescription": "Child line list description", + "id": "s85urBIkN0z" +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt new file mode 100644 index 0000000000..b2b45d9226 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class TrackerVisualizationShould : BaseObjectShould("visualization/tracker_visualization.json"), ObjectShould { + + @Test + override fun map_from_json_string() { + val visualization = objectMapper.readValue(jsonStream, TrackerVisualization::class.java) + + assertThat(visualization.uid()).isEqualTo("s85urBIkN0z") + assertThat(visualization.name()).isEqualTo("TB program") + assertThat(visualization.displayName()).isEqualTo("TB program") + assertThat(visualization.description()).isEqualTo("Line list for TB program") + assertThat(visualization.displayDescription()).isEqualTo("Line list for TB program") + assertThat(visualization.created()).isEqualTo(DateUtils.DATE_FORMAT.parse("2024-02-07T07:37:41.116")) + assertThat(visualization.lastUpdated()).isEqualTo(DateUtils.DATE_FORMAT.parse("2024-02-07T07:43:25.556")) + assertThat(visualization.type()).isEqualTo(TrackerVisualizationType.LINE_LIST) + assertThat(visualization.outputType()).isEqualTo(TrackerVisualizationOutputType.ENROLLMENT) + assertThat(visualization.program()!!.uid()).isEqualTo("ur1Edk5Oe2n") + + assertThat(visualization.columns()?.size).isEqualTo(4) + val dataElementColumn = visualization.columns()!!.find { it.dimensionType() == "PROGRAM_DATA_ELEMENT" }!! + assertThat(dataElementColumn.dimension()).isEqualTo("fCXKBdc27Bt") + assertThat(dataElementColumn.program()!!.uid()).isEqualTo("ur1Edk5Oe2n") + assertThat(dataElementColumn.programStage()!!.uid()).isEqualTo("EPEcjy3FWmI") + assertThat(dataElementColumn.filter()).isEqualTo("IN:1") + assertThat(dataElementColumn.repetition()!!.indexes()).isEqualTo(listOf(1, 2, -2, -1, 0)) + + assertThat(visualization.filters()?.size).isEqualTo(1) + assertThat(visualization.filters()!![0].dimension()).isEqualTo("enrollmentDate") + assertThat(visualization.filters()!![0].dimensionType()).isEqualTo("PERIOD") + assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("LAST_5_YEARS") + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt new file mode 100644 index 0000000000..4690a890fb --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import com.nhaarman.mockitokotlin2.* +import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class TrackerVisualizationHandlerShould { + + private val store: TrackerVisualizationStore = mock() + private val collectionCleaner: TrackerVisualizationCollectionCleaner = mock() + private val dimensionHandler: TrackerVisualizationDimensionHandler = mock() + private val dimension: TrackerVisualizationDimension = TrackerVisualizationDimension.builder().build() + private val trackerVisualization: TrackerVisualization = mock() + + // object to test + private lateinit var trackerVisualizationHandler: TrackerVisualizationHandler + + @Before + fun setUp() { + trackerVisualizationHandler = TrackerVisualizationHandler( + store, + collectionCleaner, + dimensionHandler, + ) + + whenever(trackerVisualization.columns()).doReturn(listOf(dimension)) + whenever(trackerVisualization.filters()).doReturn(listOf(dimension)) + whenever(store.updateOrInsert(any())).doReturn(HandleAction.Insert) + whenever(trackerVisualization.uid()).doReturn("tracker_visualization_uid") + } + + @Test + fun call_items_handler() { + trackerVisualizationHandler.handleMany(listOf(trackerVisualization)) + verify(dimensionHandler).handleMany(any(), any(), any()) + } + + @Test + fun call_collection_cleaner() { + trackerVisualizationHandler.handleMany(listOf(trackerVisualization)) + verify(collectionCleaner).deleteNotPresent(any()) + } +} From 777836e71bb154e641fe0858d314cb3e5952c921 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 12:39:47 +0100 Subject: [PATCH 050/222] [ANDROSDK-1810] Add additional foreign key constraints --- core/src/main/assets/migrations/158.sql | 4 ++-- core/src/main/assets/snapshots/snapshot.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/assets/migrations/158.sql b/core/src/main/assets/migrations/158.sql index cd9e8c00d5..b804f08508 100644 --- a/core/src/main/assets/migrations/158.sql +++ b/core/src/main/assets/migrations/158.sql @@ -1,4 +1,4 @@ # Add TrackerVisualization model (ANDROSDK-1810) -CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); -CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 03203473e7..a0c82e4a46 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -130,5 +130,5 @@ CREATE TABLE DataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NO CREATE TABLE ProgramStageWorkingList (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, eventStatus TEXT, eventCreatedAt TEXT, eventOccurredAt TEXT, eventScheduledAt TEXT, enrollmentStatus TEXT, enrolledAt TEXT, enrollmentOccurredAt TEXT, orderProperty TEXT, displayColumnOrder TEXT, orgUnit TEXT, ouMode TEXT, assignedUserMode TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (orgUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadURL TEXT, version TEXT); CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); -CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); -CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); From e5295ec444f696166344df397fe2fbce61b36429 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 14:05:01 +0100 Subject: [PATCH 051/222] [ANDROSDK-1810] Adapt unit test --- .../dhis/android/core/domain/metadata/MetadataCallShould.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt index 8061a1f6c4..6e63293cd0 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt @@ -60,6 +60,7 @@ import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoModuleDownloader import org.hisp.dhis.android.core.usecase.UseCaseModuleDownloader import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.internal.UserModuleDownloader +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationModuleDownloader import org.hisp.dhis.android.core.visualization.internal.VisualizationModuleDownloader import org.junit.Assert.fail import org.junit.Before @@ -81,6 +82,7 @@ class MetadataCallShould : BaseCallShould() { private val organisationUnitDownloader: OrganisationUnitModuleDownloader = mock() private val dataSetDownloader: DataSetModuleDownloader = mock() private val visualizationDownloader: VisualizationModuleDownloader = mock() + private val trackerVisualizationDownloader: TrackerVisualizationModuleDownloader = mock() private val constantDownloader: ConstantModuleDownloader = mock() private val indicatorDownloader: IndicatorModuleDownloader = mock() private val programIndicatorModuleDownloader: ProgramIndicatorModuleDownloader = mock() @@ -136,6 +138,9 @@ class MetadataCallShould : BaseCallShould() { visualizationDownloader.stub { onBlocking { downloadMetadata() }.doReturn(emptyList()) } + trackerVisualizationDownloader.stub { + onBlocking { downloadMetadata() }.doReturn(emptyList()) + } legendSetModuleDownloader.stub { onBlocking { downloadMetadata() }.doReturn(Unit) } @@ -173,6 +178,7 @@ class MetadataCallShould : BaseCallShould() { organisationUnitDownloader, dataSetDownloader, visualizationDownloader, + trackerVisualizationDownloader, constantDownloader, indicatorDownloader, programIndicatorModuleDownloader, From f9352d70a495a845e3938f5d604bfa1c166172ba Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 15:12:35 +0100 Subject: [PATCH 052/222] [ANDROSDK-1810] Fix sonar code smells --- .../dhis/android/core/visualization/TrackerVisualization.java | 2 +- .../core/visualization/internal/TrackerVisualizationCall.kt | 2 -- .../internal/TrackerVisualizationModuleDownloader.kt | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java index d7be657254..1b0b416c8a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java @@ -100,7 +100,7 @@ public static Builder builder() { } public static TrackerVisualization create(Cursor cursor) { - return AutoValue_TrackerVisualization.createFromCursor(cursor); + return $AutoValue_TrackerVisualization.createFromCursor(cursor); } public abstract Builder toBuilder(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt index ae4855d602..a6c7ae9101 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt @@ -52,8 +52,6 @@ internal class TrackerVisualizationCall( override suspend fun download(uids: Set): List { val accessFilter = "access." + AccessFields.read.eq(true).generateString() - // TODO Limit by version - return if (dhis2VersionManager.isGreaterOrEqualThan(DHISVersion.V2_38)) { apiDownloader.downloadPartitioned( uids, diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt index f419ddfc09..5fb6459119 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt @@ -38,7 +38,7 @@ internal class TrackerVisualizationModuleDownloader internal constructor( TypedModuleDownloader> { override suspend fun downloadMetadata(): List { - // TODO Extract tracker visualization uids from analytics settings + // Extract visualizations in ANDROSDK-1811 val trackerVisualizations = setOf("s85urBIkN0z") return visualizationCall.download(trackerVisualizations) } From 6e27540efb39c9cba598974ef6b38bc733c78400 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 8 Feb 2024 11:57:38 +0100 Subject: [PATCH 053/222] [DISABLE-CONCURRENT-BUILDS] Disable concurrent builds --- Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index fe0e0853db..c0d8ae698e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,10 @@ pipeline { label "ec2-android" } + options { + disableConcurrentBuilds(abortPrevious: true) + } + stages{ stage('Change to JAVA 17') { steps { From 755bffe7dc99c8ce52c8964a75fe91352451c2b6 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:30:21 +0100 Subject: [PATCH 054/222] [DEVELOP] Update MapLayer fields --- .../internal/ImageFormatColumnAdapter.kt | 36 +++++++++++++++++++ .../enums/internal/MapServiceColumnAdapter.kt | 36 +++++++++++++++++++ .../dhis/android/core/map/layer/MapLayer.java | 24 +++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/ImageFormatColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/MapServiceColumnAdapter.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/ImageFormatColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/ImageFormatColumnAdapter.kt new file mode 100644 index 0000000000..7ea017a3fe --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/ImageFormatColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.map.layer.ImageFormat + +internal class ImageFormatColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return ImageFormat::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/MapServiceColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/MapServiceColumnAdapter.kt new file mode 100644 index 0000000000..196ab4754d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/MapServiceColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.map.layer.MapService + +internal class MapServiceColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return MapService::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java index 4ca7bacfe7..2b2b723118 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java @@ -37,7 +37,9 @@ import com.google.auto.value.AutoValue; import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.ImageFormatColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.MapLayerPositionColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.MapServiceColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreMapLayerImageryProviderColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.ObjectWithUidInterface; @@ -83,6 +85,20 @@ public abstract class MapLayer extends BaseObject implements ObjectWithUidInterf @ColumnAdapter(IgnoreMapLayerImageryProviderColumnAdapter.class) public abstract List imageryProviders(); + @NonNull + public abstract String code(); + + @NonNull + @ColumnAdapter(MapServiceColumnAdapter.class) + public abstract MapService mapService(); + + @NonNull + @ColumnAdapter(ImageFormatColumnAdapter.class) + public abstract ImageFormat imageFormat(); + + @NonNull + public abstract String layers(); + public static MapLayer create(Cursor cursor) { return $AutoValue_MapLayer.createFromCursor(cursor); } @@ -102,6 +118,8 @@ public abstract static class Builder extends BaseObject.Builder { public abstract Builder displayName(String displayName); + public abstract Builder code(String code); + public abstract Builder external(Boolean external); public abstract Builder mapLayerPosition(MapLayerPosition mapLayerPosition); @@ -116,6 +134,12 @@ public abstract static class Builder extends BaseObject.Builder { public abstract Builder imageryProviders(List imageryProviders); + public abstract Builder mapService(MapService mapService); + + public abstract Builder imageFormat(ImageFormat imageFormat); + + public abstract Builder layers(String layers); + public abstract MapLayer build(); } } From 3a426b20996b365c9ba73136aa0c1797aed1f1af Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:31:02 +0100 Subject: [PATCH 055/222] [DEVELOP] Update MapLayer table info --- .../hisp/dhis/android/core/map/layer/MapLayerTableInfo.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerTableInfo.kt index 4104a06da3..0a98400fff 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerTableInfo.kt @@ -56,6 +56,10 @@ object MapLayerTableInfo { IMAGE_URL, SUBDOMAINS, SUBDOMAIN_PLACEHOLDER, + CODE, + MAP_SERVICE, + IMAGE_FORMAT, + LAYERS, ) } @@ -69,6 +73,10 @@ object MapLayerTableInfo { const val IMAGE_URL = "imageUrl" const val SUBDOMAINS = "subdomains" const val SUBDOMAIN_PLACEHOLDER = "subdomainPlaceholder" + const val CODE = "code" + const val MAP_SERVICE = "mapService" + const val IMAGE_FORMAT = "imageFormat" + const val LAYERS = "layers" } } } From c6532a7619fde0b73e87ae9605753b9747c5d512 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:31:37 +0100 Subject: [PATCH 056/222] [DEVELOP] Update MapLayer table info --- .../android/core/map/layer/ImageFormat.kt | 33 +++++++++++++++++++ .../dhis/android/core/map/layer/MapService.kt | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/ImageFormat.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/MapService.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/ImageFormat.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/ImageFormat.kt new file mode 100644 index 0000000000..310e7c8031 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/ImageFormat.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.map.layer + +enum class ImageFormat { + PNG, JPG +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapService.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapService.kt new file mode 100644 index 0000000000..aa8c74f234 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapService.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.map.layer + +enum class MapService { + TMS, XYZ, WMS, VECTOR_STYLE +} From 653ea8e4ef5b324aa8d1fc4cf9be64bb1d079f54 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:32:18 +0100 Subject: [PATCH 057/222] [DEVELOP] Update MapLayer store --- .../dhis/android/core/map/layer/internal/MapLayerStoreImpl.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerStoreImpl.kt index 7fc34ad18c..d85079890f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerStoreImpl.kt @@ -60,6 +60,10 @@ internal class MapLayerStoreImpl( w.bind(7, o.imageUrl()) w.bind(8, StringListColumnAdapter.serialize(o.subdomains())) w.bind(9, o.subdomainPlaceholder()) + w.bind(10, o.code()) + w.bind(11, o.mapService()) + w.bind(12, o.imageFormat()) + w.bind(13, o.layers()) } } } From 107c92ee5a7b82551b2107348174f96cd4338c1c Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:32:50 +0100 Subject: [PATCH 058/222] [DEVELOP] Update MapLayer database and migrations --- core/src/main/assets/migrations/158.sql | 6 ++++++ core/src/main/assets/snapshots/snapshot.sql | 2 +- .../core/arch/db/access/internal/BaseDatabaseOpenHelper.kt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 core/src/main/assets/migrations/158.sql diff --git a/core/src/main/assets/migrations/158.sql b/core/src/main/assets/migrations/158.sql new file mode 100644 index 0000000000..549ae49fad --- /dev/null +++ b/core/src/main/assets/migrations/158.sql @@ -0,0 +1,6 @@ +# Add external map layers (ANDROSDK-1800) + +ALTER TABLE MapLayer ADD COLUMN code TEXT; +ALTER TABLE MapLayer ADD COLUMN mapService TEXT; +ALTER TABLE MapLayer ADD COLUMN imageFormat TEXT; +ALTER TABLE MapLayer ADD COLUMN layers TEXT; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index f6fdcef9de..3ac0fc841c 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -124,7 +124,7 @@ CREATE TABLE TrackedEntityAttributeLegendSetLink (_id INTEGER PRIMARY KEY AUTOIN CREATE TABLE ItemFilter (_id INTEGER PRIMARY KEY AUTOINCREMENT, eventFilter TEXT, dataItem TEXT, trackedEntityInstanceFilter TEXT, attribute TEXT, programStageWorkingList TEXT, sw TEXT, ew TEXT, le TEXT, ge TEXT, gt TEXT, lt TEXT, eq TEXT, inProperty TEXT, like TEXT, dateFilter TEXT, FOREIGN KEY (eventFilter) REFERENCES EventFilter (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityInstanceFilter) REFERENCES TrackedEntityInstanceFilter (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStageWorkingList) REFERENCES ProgramStageWorkingList (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE StockUseCase (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, itemCode TEXT, itemDescription TEXT, programType TEXT, description TEXT, stockOnHand TEXT, FOREIGN KEY (uid) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE StockUseCaseTransaction (_id INTEGER PRIMARY KEY AUTOINCREMENT, programUid TEXT NOT NULL, sortOrder INTEGER, transactionType TEXT, distributedTo TEXT, stockDistributed TEXT, stockDiscarded TEXT, stockCount TEXT, FOREIGN KEY (programUid) REFERENCES StockUseCase (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE MapLayer (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, name TEXT NOT NULL, displayName TEXT NOT NULL, external INTEGER, mapLayerPosition TEXT NOT NULL, style TEXT, imageUrl TEXT NOT NULL, subdomains TEXT, subdomainPlaceholder TEXT); +CREATE TABLE MapLayer (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, name TEXT NOT NULL, displayName TEXT NOT NULL, external INTEGER, mapLayerPosition TEXT NOT NULL, style TEXT, imageUrl TEXT NOT NULL, subdomains TEXT, subdomainPlaceholder TEXT, code TEXT, mapService TEXT, imageFormat TEXT, layers TEXT); CREATE TABLE MapLayerImageryProvider (_id INTEGER PRIMARY KEY AUTOINCREMENT, mapLayer TEXT NOT NULL, attribution TEXT NOT NULL, coverageAreas TEXT, FOREIGN KEY (mapLayer) REFERENCES MapLayer (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE DataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT, syncState TEXT, deleted INTEGER, UNIQUE(namespace, key)); CREATE TABLE ProgramStageWorkingList (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, eventStatus TEXT, eventCreatedAt TEXT, eventOccurredAt TEXT, eventScheduledAt TEXT, enrollmentStatus TEXT, enrolledAt TEXT, enrollmentOccurredAt TEXT, orderProperty TEXT, displayColumnOrder TEXT, orgUnit TEXT, ouMode TEXT, assignedUserMode TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (orgUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index 6226e4b1fd..604c32b648 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 157 + const val VERSION = 158 } } From 67d11f609bc8fea5001f3a58216e4390d8b454ef Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:33:07 +0100 Subject: [PATCH 059/222] [DEVELOP] Add external map layer --- .../internal/externalmap/ExternalMapLayer.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt new file mode 100644 index 0000000000..19e76e64d8 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.map.layer.internal.externalmap + +import org.hisp.dhis.android.core.map.layer.ImageFormat +import org.hisp.dhis.android.core.map.layer.MapLayerPosition +import org.hisp.dhis.android.core.map.layer.MapService + +data class ExternalMapLayer( + val id: String, + val name: String, + val displayName: String, + val code: String, + val url: String, + val attribution: String, + val mapService: MapService, + val imageFormat: ImageFormat, + val layers: String, + val mapLayerPosition: MapLayerPosition, +) From 6e41790d2b15b107b18786ab663a194a47d3f6e2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:33:18 +0100 Subject: [PATCH 060/222] [DEVELOP] Add external map layer fields --- .../externalmap/ExternalMapLayerFields.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt new file mode 100644 index 0000000000..b858dc923c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.map.layer.internal.externalmap + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.map.layer.MapLayerImageryProviderTableInfo +import org.hisp.dhis.android.core.map.layer.MapLayerTableInfo + +internal object ExternalMapLayerFields { + + private const val URL = "url" + + private val fh = FieldsHelper() + val uid = fh.uid() + + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(URL), + fh.field(MapLayerImageryProviderTableInfo.Columns.ATTRIBUTION), + fh.field(MapLayerTableInfo.Columns.MAP_SERVICE), + fh.field(MapLayerTableInfo.Columns.IMAGE_FORMAT), + fh.field(MapLayerTableInfo.Columns.LAYERS), + fh.field(MapLayerTableInfo.Columns.MAP_LAYER_POSITION), + ).build() +} From fc7cdd8e6e4d8c0ae2e1e4ea48c12454bb84b1ad Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:33:36 +0100 Subject: [PATCH 061/222] [DEVELOP] Add external map layer service and call factory --- .../ExternalMapLayerCallFactory.kt | 78 +++++++++++++++++++ .../externalmap/ExternalMapLayerService.kt | 44 +++++++++++ 2 files changed, 122 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt new file mode 100644 index 0000000000..f6408eb505 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.map.layer.internal.externalmap + +import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader +import org.hisp.dhis.android.core.map.layer.MapLayer +import org.hisp.dhis.android.core.map.layer.MapLayerImageryProvider +import org.hisp.dhis.android.core.map.layer.MapLayerPosition +import org.hisp.dhis.android.core.map.layer.internal.MapLayerHandler +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitLevel +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelFields +import org.koin.core.annotation.Singleton + +@Singleton +internal class ExternalMapLayerCallFactory( + private val mapLayerHandler: MapLayerHandler, + val apiDownloader: APIDownloader, + val service: ExternalMapLayerService, +) { + + suspend fun download(): List { + val mapLayers = getExternalMapLayers() + mapLayerHandler.handleMany(mapLayers) + return mapLayers + } + + private suspend fun getExternalMapLayers(): List { + return service.getExternalMapLayers(ExternalMapLayerFields.allFields, false).items() + .map { externalMapLayer -> + MapLayer.builder() + .uid(externalMapLayer.id) + .name(externalMapLayer.name) + .displayName(externalMapLayer.displayName) + .code(externalMapLayer.code) + .mapLayerPosition(externalMapLayer.mapLayerPosition) + .mapService(externalMapLayer.mapService) + .imageFormat(externalMapLayer.imageFormat) + .layers(externalMapLayer.layers) + .external(true) + .imageUrl(externalMapLayer.url) + .imageryProviders( + listOf( + MapLayerImageryProvider.builder() + .mapLayer(externalMapLayer.id) + .attribution(externalMapLayer.attribution) + .build(), + ), + ) + .build() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt new file mode 100644 index 0000000000..5a065c454a --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.map.layer.internal.externalmap + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface ExternalMapLayerService { + + @GET("externalMapLayers") + suspend fun getExternalMapLayers( + @Query("fields") @Which fields: Fields, + @Query("paging") paging: Boolean, + ): Payload +} From f43a2cc56a8a8ff7eba3155edcddc391fd5af495 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:33:56 +0100 Subject: [PATCH 062/222] [DEVELOP] Add map layer collection cleaner --- .../internal/MapLayerCollectionCleaner.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt new file mode 100644 index 0000000000..ca356707b7 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.map.layer.internal + +import org.hisp.dhis.android.core.arch.cleaners.internal.CollectionCleanerImpl +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.map.layer.MapLayer +import org.hisp.dhis.android.core.map.layer.MapLayerTableInfo +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class MapLayerCollectionCleaner( + databaseAdapter: DatabaseAdapter, +) : CollectionCleanerImpl( + tableName = MapLayerTableInfo.TABLE_INFO.name(), + databaseAdapter = databaseAdapter, +) From 30d24022cafd219707685c3eb4101f9aa8b07ecc Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:34:15 +0100 Subject: [PATCH 063/222] [DEVELOP] update map layer call factory --- .../android/core/map/layer/internal/MapLayerCallFactory.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCallFactory.kt index 6831154055..93e86561a3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCallFactory.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import org.hisp.dhis.android.core.map.layer.MapLayer import org.hisp.dhis.android.core.map.layer.internal.bing.BingCallFactory +import org.hisp.dhis.android.core.map.layer.internal.externalmap.ExternalMapLayerCallFactory import org.hisp.dhis.android.core.map.layer.internal.osm.OSMCallFactory import org.koin.core.annotation.Singleton @@ -38,10 +39,14 @@ import org.koin.core.annotation.Singleton internal class MapLayerCallFactory( private val osmCallFactory: OSMCallFactory, private val bingCallFactory: BingCallFactory, + private val externalMapLayerCallFactory: ExternalMapLayerCallFactory, + private val mapLayerCollectionCleaner: MapLayerCollectionCleaner, ) { suspend fun downloadMetadata(): List { - return flowOf(osmCallFactory.download(), bingCallFactory.download()).toList() + return flowOf(osmCallFactory.download(), bingCallFactory.download(), externalMapLayerCallFactory.download()) + .toList() .flatten() + .also { mapLayerCollectionCleaner.deleteNotPresent(it) } } } From 965a13d77b073024232a76db7ea5de58893e9518 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:34:35 +0100 Subject: [PATCH 064/222] [DEVELOP] Update map layer collection repository --- .../map/layer/MapLayerCollectionRepository.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt index f45fad005a..857706c6aa 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt @@ -37,6 +37,7 @@ import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilte import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.map.layer.internal.MapLayerImagerProviderChildrenAppender import org.hisp.dhis.android.core.map.layer.internal.MapLayerStore +import org.hisp.dhis.android.core.map.layer.internal.bing.ImageryProvider import org.koin.core.annotation.Singleton @Singleton @@ -66,6 +67,10 @@ class MapLayerCollectionRepository internal constructor( return cf.string(MapLayerTableInfo.Columns.DISPLAY_NAME) } + fun byCode(): StringFilterConnector { + return cf.string(MapLayerTableInfo.Columns.CODE) + } + fun byExternal(): BooleanFilterConnector { return cf.bool(MapLayerTableInfo.Columns.EXTERNAL) } @@ -86,6 +91,18 @@ class MapLayerCollectionRepository internal constructor( return cf.withChild(MapLayer.IMAGERY_PROVIDERS) } + fun byMapService(): EnumFilterConnector { + return cf.enumC(MapLayerTableInfo.Columns.MAP_SERVICE) + } + + fun byImageFormat(): EnumFilterConnector { + return cf.enumC(MapLayerTableInfo.Columns.IMAGE_FORMAT) + } + + fun byLayers(): StringFilterConnector { + return cf.string(MapLayerTableInfo.Columns.LAYERS) + } + internal companion object { val childrenAppenders: ChildrenAppenderGetter = mapOf( MapLayer.IMAGERY_PROVIDERS to ::MapLayerImagerProviderChildrenAppender, From 4f1eab7f99ea3eae9765dcfb48883c0ac587e668 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:34:49 +0100 Subject: [PATCH 065/222] [DEVELOP] Add external map layers json --- .../external_map_layers.json | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json diff --git a/core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json b/core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json new file mode 100644 index 0000000000..7497958597 --- /dev/null +++ b/core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json @@ -0,0 +1,60 @@ +{ + "pager": { + "page": 1, + "total": 4, + "pageSize": 50, + "pageCount": 1 + }, + "externalMapLayers": [ + { + "name": " Dark basemap", + "code": "DARK_BASEMAP", + "created": "2016-10-05T16:38:03.202", + "lastUpdated": "2024-02-09T10:20:17.044", + "mapService": "WMS", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", + "attribution": "© OpenStreetMap, CARTO", + "layers": "layer_test", + "imageFormat": "JPG", + "mapLayerPosition": "OVERLAY", + "displayName": " Dark basemap", + "id": "LOw2p0kPwua" + }, + { + "name": "Aerial imagery of Dar-es-Salaam", + "created": "2016-10-05T16:28:27.586", + "lastUpdated": "2024-02-09T10:06:42.677", + "mapService": "XYZ", + "url": "https://a.tiles.mapbox.com/v4/worldbank-education.pebkgmlc/{z}/{x}/{y}.png?access_token=pk.eyJ1Ijoid29ybGRiYW5rLWVkdWNhdGlvbiIsImEiOiJIZ2VvODFjIn0.TDw5VdwGavwEsch53sAVxA", + "attribution": "OpenAerialMap / Tanzania Open Data Initiative", + "imageFormat": "PNG", + "mapLayerPosition": "BASEMAP", + "displayName": "Aerial imagery of Dar-es-Salaam", + "id": "ni2ZiTOZaPD" + }, + { + "name": "Labels overlay", + "created": "2016-10-05T16:40:08.241", + "lastUpdated": "2016-10-05T16:40:08.241", + "mapService": "XYZ", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}.png", + "attribution": "© OpenStreetMap, CARTO", + "imageFormat": "PNG", + "mapLayerPosition": "OVERLAY", + "displayName": "Labels overlay", + "id": "suB1SFdc6RD" + }, + { + "name": "Terrain basemap", + "created": "2016-10-05T16:45:21.378", + "lastUpdated": "2016-10-05T16:49:13.324", + "mapService": "WMS", + "url": "https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png", + "attribution": "Stamen Design, OpenStreetMap", + "imageFormat": "PNG", + "mapLayerPosition": "BASEMAP", + "displayName": "Terrain basemap", + "id": "wNIQ8pNvSQd" + } + ] +} \ No newline at end of file From 27b68dca3fa12d50dece0f9c0af790d27a5decec Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 9 Feb 2024 12:35:28 +0100 Subject: [PATCH 066/222] [DEVELOP] Update map layer tests --- ...llectionRepositoryMockIntegrationShould.kt | 43 ++++++++++++++++++- .../android/core/data/maps/MapLayerSamples.kt | 1 + 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt index 3a3e1e90fd..854b3b2b6a 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt @@ -29,7 +29,9 @@ package org.hisp.dhis.android.testapp.map.layer import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.map.layer.ImageFormat import org.hisp.dhis.android.core.map.layer.MapLayerPosition +import org.hisp.dhis.android.core.map.layer.MapService import org.hisp.dhis.android.core.map.layer.internal.bing.BingBasemaps import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestEmptyEnqueable import org.hisp.dhis.android.core.utils.runner.D2JunitRunner @@ -45,7 +47,7 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes val mapLayers = d2.mapsModule().mapLayers() .blockingGet() - assertThat(mapLayers.size).isEqualTo(5) + assertThat(mapLayers.size).isEqualTo(9) } @Test @@ -75,6 +77,15 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes assertThat(mapLayers.size).isEqualTo(1) } + @Test + fun filter_by_code() { + val mapLayers = d2.mapsModule().mapLayers() + .byCode().eq("DARK_BASEMAP") + .blockingGet() + + assertThat(mapLayers.size).isEqualTo(1) + } + @Test fun filter_by_external() { val mapLayers = d2.mapsModule().mapLayers() @@ -90,7 +101,7 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes .byMapLayerPosition().eq(MapLayerPosition.BASEMAP) .blockingGet() - assertThat(mapLayers.size).isEqualTo(5) + assertThat(mapLayers.size).isEqualTo(9) } @Test @@ -112,6 +123,33 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes assertThat(mapLayers.first().imageryProviders()).isNotEmpty() } + @Test + fun filter_by_map_service() { + val mapLayers = d2.mapsModule().mapLayers() + .byMapService().eq(MapService.WMS) + .blockingGet() + + assertThat(mapLayers.size).isEqualTo(2) + } + + @Test + fun filter_by_image_format() { + val mapLayers = d2.mapsModule().mapLayers() + .byImageFormat().eq(ImageFormat.JPG) + .blockingGet() + + assertThat(mapLayers.size).isEqualTo(1) + } + + @Test + fun filter_by_layers() { + val mapLayers = d2.mapsModule().mapLayers() + .byLayers().eq("layer_test") + .blockingGet() + + assertThat(mapLayers.size).isEqualTo(1) + } + companion object { @BeforeClass @JvmStatic @@ -123,6 +161,7 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes dhis2MockServer.enqueueMockResponse("map/layer/bing/bing_server_response.json") dhis2MockServer.enqueueMockResponse(401) dhis2MockServer.enqueueMockResponse("map/layer/bing/bing_server_response.json") + dhis2MockServer.enqueueMockResponse("map/layer/externalmap/external_map_layers.json") d2.mapsModule().mapLayersDownloader().downloadMetadata().blockingAwait() } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt index b9e373b835..dc432ec176 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt @@ -36,6 +36,7 @@ object MapLayerSamples { .id(1L) .uid("map_layer_uid") .name("Map Layer") + .code("MAP_CODE") .displayName("Display map layer") .external(true) .mapLayerPosition(MapLayerPosition.BASEMAP) From a7bdb9684a34d5ac2a10b30e72b75f13212fda5d Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 12 Feb 2024 09:13:09 +0100 Subject: [PATCH 067/222] [ANDROSDK-1800] PMD and KtlintFormat --- .../android/core/map/layer/MapLayerCollectionRepository.kt | 2 +- .../core/map/layer/internal/MapLayerCollectionCleaner.kt | 2 -- .../internal/externalmap/ExternalMapLayerCallFactory.kt | 3 --- .../org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt | 5 +++++ 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt index 857706c6aa..3682237eea 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayerCollectionRepository.kt @@ -37,9 +37,9 @@ import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilte import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.map.layer.internal.MapLayerImagerProviderChildrenAppender import org.hisp.dhis.android.core.map.layer.internal.MapLayerStore -import org.hisp.dhis.android.core.map.layer.internal.bing.ImageryProvider import org.koin.core.annotation.Singleton +@Suppress("TooManyFunctions") @Singleton class MapLayerCollectionRepository internal constructor( store: MapLayerStore, diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt index ca356707b7..5969ab9995 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/MapLayerCollectionCleaner.kt @@ -31,8 +31,6 @@ import org.hisp.dhis.android.core.arch.cleaners.internal.CollectionCleanerImpl import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.map.layer.MapLayer import org.hisp.dhis.android.core.map.layer.MapLayerTableInfo -import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.android.core.program.ProgramTableInfo import org.koin.core.annotation.Singleton @Singleton diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt index f6408eb505..19b7267edb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt @@ -31,10 +31,7 @@ package org.hisp.dhis.android.core.map.layer.internal.externalmap import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader import org.hisp.dhis.android.core.map.layer.MapLayer import org.hisp.dhis.android.core.map.layer.MapLayerImageryProvider -import org.hisp.dhis.android.core.map.layer.MapLayerPosition import org.hisp.dhis.android.core.map.layer.internal.MapLayerHandler -import org.hisp.dhis.android.core.organisationunit.OrganisationUnitLevel -import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelFields import org.koin.core.annotation.Singleton @Singleton diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt index dc432ec176..0a98d43e7c 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/maps/MapLayerSamples.kt @@ -27,8 +27,10 @@ */ package org.hisp.dhis.android.core.data.maps +import org.hisp.dhis.android.core.map.layer.ImageFormat import org.hisp.dhis.android.core.map.layer.MapLayer import org.hisp.dhis.android.core.map.layer.MapLayerPosition +import org.hisp.dhis.android.core.map.layer.MapService object MapLayerSamples { fun get(): MapLayer { @@ -44,6 +46,9 @@ object MapLayerSamples { .imageUrl("https://provider-{s}.url") .subdomains(listOf("a", "b", "c")) .subdomainPlaceholder("{s}") + .imageFormat(ImageFormat.JPG) + .layers("layer") + .mapService(MapService.TMS) .build() } } From 99259afa51e4f90e69ee89d65fb683cf6fac016c Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 8 Feb 2024 11:57:38 +0100 Subject: [PATCH 068/222] [DISABLE-CONCURRENT-BUILDS] Disable concurrent builds --- Jenkinsfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index fe0e0853db..c0d8ae698e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,10 @@ pipeline { label "ec2-android" } + options { + disableConcurrentBuilds(abortPrevious: true) + } + stages{ stage('Change to JAVA 17') { steps { From fff16d6329651f85ece2da885abd59ef4b324c4d Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 6 Feb 2024 13:51:07 +0100 Subject: [PATCH 069/222] [ANDROSDK-1810] Add TrackerVisualization model --- ...izationDimensionRepetitionColumnAdapter.kt | 51 +++++++ ...kerVisualizationOutputTypeColumnAdapter.kt | 36 +++++ .../TrackerVisualizationTypeColumnAdapter.kt | 36 +++++ .../ObjectWithUidListColumnAdapter.kt | 50 +++++++ ...sualizationDimensionListColumnAdapter.java | 37 +++++ .../visualization/TrackerVisualization.java | 134 ++++++++++++++++++ .../TrackerVisualizationDimension.java | 125 ++++++++++++++++ ...ackerVisualizationDimensionRepetition.java | 62 ++++++++ .../TrackerVisualizationOutputType.kt | 35 +++++ .../visualization/TrackerVisualizationType.kt | 49 +++++++ 10 files changed, 615 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt new file mode 100644 index 0000000000..7c1056dd75 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/custom/internal/TrackerVisualizationDimensionRepetitionColumnAdapter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.custom.internal + +import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +internal class TrackerVisualizationDimensionRepetitionColumnAdapter : + JSONObjectColumnAdapter() { + + override fun getEnumClass(): Class { + return TrackerVisualizationDimensionRepetition::class.java + } + + override fun serialize(o: TrackerVisualizationDimensionRepetition?): String? { + return TrackerVisualizationDimensionRepetitionColumnAdapter.serialize(o) + } + + companion object { + fun serialize(o: TrackerVisualizationDimensionRepetition?): String? { + return o?.let { + ObjectMapperFactory.objectMapper().writeValueAsString(it) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt new file mode 100644 index 0000000000..2427c364ec --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationOutputTypeColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType + +class TrackerVisualizationOutputTypeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return TrackerVisualizationOutputType::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt new file mode 100644 index 0000000000..9cde39b30d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/TrackerVisualizationTypeColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType + +class TrackerVisualizationTypeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return TrackerVisualizationType::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt new file mode 100644 index 0000000000..d5e7f5aff9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal + +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.JSONObjectListColumnAdapter +import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory +import org.hisp.dhis.android.core.common.ObjectWithUid + +internal class ObjectWithUidListColumnAdapter : JSONObjectListColumnAdapter() { + override fun getObjectClass(): Class> { + return ArrayList().javaClass + } + + override fun serialize(o: List?): String? { + return ObjectWithUidListColumnAdapter.serialize(o) + } + + companion object { + fun serialize(o: List?): String? { + return o?.let { + ObjectMapperFactory.objectMapper().writeValueAsString(it) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java new file mode 100644 index 0000000000..b8abcc88e5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreTrackerVisualizationDimensionListColumnAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal; + +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension; + +import java.util.List; + +public final class IgnoreTrackerVisualizationDimensionListColumnAdapter + extends IgnoreColumnAdapter> { +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java new file mode 100644 index 0000000000..d7be657254 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import android.database.Cursor; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.TrackerVisualizationOutputTypeColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.TrackerVisualizationTypeColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreTrackerVisualizationDimensionListColumnAdapter; +import org.hisp.dhis.android.core.common.BaseIdentifiableObject; +import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectWithUid; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = $$AutoValue_TrackerVisualization.Builder.class) +public abstract class TrackerVisualization extends BaseIdentifiableObject implements CoreObject { + + @Nullable + @JsonProperty() + public abstract String description(); + + @Nullable + @JsonProperty() + public abstract String displayDescription(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationTypeColumnAdapter.class) + public abstract TrackerVisualizationType type(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationOutputTypeColumnAdapter.class) + public abstract TrackerVisualizationOutputType outputType(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid program(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid programStage(); + + @Nullable + @JsonProperty + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid trackedEntityType(); + + @Nullable + @JsonProperty() + @ColumnAdapter(IgnoreTrackerVisualizationDimensionListColumnAdapter.class) + public abstract List columns(); + + @Nullable + @JsonProperty() + @ColumnAdapter(IgnoreTrackerVisualizationDimensionListColumnAdapter.class) + public abstract List filters(); + + public static Builder builder() { + return new $$AutoValue_TrackerVisualization.Builder(); + } + + public static TrackerVisualization create(Cursor cursor) { + return AutoValue_TrackerVisualization.createFromCursor(cursor); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder extends BaseIdentifiableObject.Builder { + + public abstract Builder id(Long id); + + public abstract Builder description(String description); + + public abstract Builder displayDescription(String displayDescription); + + public abstract Builder type(TrackerVisualizationType type); + + public abstract Builder outputType(TrackerVisualizationOutputType type); + + public abstract Builder program(ObjectWithUid program); + + public abstract Builder programStage(ObjectWithUid programStage); + + public abstract Builder trackedEntityType(ObjectWithUid trackedEntityType); + + public abstract Builder columns(List columns); + + public abstract Builder filters(List filters); + + public abstract TrackerVisualization build(); + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java new file mode 100644 index 0000000000..f7641c6dbf --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.LayoutPositionColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidListColumnAdapter; +import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectWithUid; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = AutoValue_TrackerVisualizationDimension.Builder.class) +public abstract class TrackerVisualizationDimension implements CoreObject { + + @Nullable + public abstract String trackerVisualization(); + + @Nullable + @ColumnAdapter(LayoutPositionColumnAdapter.class) + public abstract LayoutPosition position(); + + @Nullable + @JsonProperty() + public abstract String dimension(); + + @Nullable + @JsonProperty() + public abstract String dimensionType(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid program(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid programStage(); + + @Nullable + @JsonProperty() + @ColumnAdapter(ObjectWithUidListColumnAdapter.class) + public abstract List items(); + + @Nullable + @JsonProperty() + public abstract String filter(); + + @Nullable + @JsonProperty() + @ColumnAdapter(TrackerVisualizationDimensionRepetitionColumnAdapter.class) + public abstract TrackerVisualizationDimensionRepetition repetition(); + + + public static Builder builder() { + return new AutoValue_TrackerVisualizationDimension.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder id(Long id); + + public abstract Builder trackerVisualization(String trackerVisualization); + + public abstract Builder position(LayoutPosition position); + + public abstract Builder dimension(String dimension); + + public abstract Builder dimensionType(String dimensionType); + + public abstract Builder program(ObjectWithUid program); + + public abstract Builder programStage(ObjectWithUid programStage); + + public abstract Builder items(List items); + + public abstract Builder filter(String filter); + + public abstract Builder repetition(TrackerVisualizationDimensionRepetition repetition); + + public abstract TrackerVisualizationDimension build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java new file mode 100644 index 0000000000..7ca98f2e42 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionRepetition.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization; + +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +import java.util.List; + +@AutoValue +@JsonDeserialize(builder = AutoValue_TrackerVisualizationDimensionRepetition.Builder.class) +public abstract class TrackerVisualizationDimensionRepetition { + + @Nullable + @JsonProperty() + public abstract List indexes(); + + public static Builder builder() { + return new AutoValue_TrackerVisualizationDimensionRepetition.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder indexes(List indexes); + + public abstract TrackerVisualizationDimensionRepetition build(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt new file mode 100644 index 0000000000..ec0ed8f92d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationOutputType.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +enum class TrackerVisualizationOutputType { + EVENT, + ENROLLMENT, + TRACKED_ENTITY_INSTANCE, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt new file mode 100644 index 0000000000..f4a3c7b431 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationType.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +enum class TrackerVisualizationType { + COLUMN, + STACKED_COLUMN, + BAR, + STACKED_BAR, + LINE, + LINE_LIST, + AREA, + STACKED_AREA, + PIE, + RADAR, + GAUGE, + YEAR_OVER_YEAR_LINE, + YEAR_OVER_YEAR_COLUMN, + SINGLE_VALUE, + PIVOT_TABLE, + SCATTER, + BUBBLE, +} From f981e1c48c6c98a8c136dae68bafb02d1209647e Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 12:30:45 +0100 Subject: [PATCH 070/222] [ANDROSDK-1810] Add TrackerVisualization store, handler and call --- .../MetadataCallMockIntegrationShould.kt | 4 +- ...lizationDimensionStoreIntegrationShould.kt | 58 ++++++++ ...ckerVisualizationStoreIntegrationShould.kt | 55 ++++++++ ...llectionRepositoryMockIntegrationShould.kt | 128 ++++++++++++++++++ core/src/main/assets/snapshots/snapshot.sql | 2 + .../arch/api/internal/ServicesDIModule.kt | 2 + .../ObjectWithUidListColumnAdapter.kt | 36 ++++- .../core/domain/metadata/MetadataCall.kt | 8 +- .../core/mockwebserver/Dhis2MockServer.java | 4 + ...rackerVisualizationCollectionRepository.kt | 100 ++++++++++++++ .../TrackerVisualizationDimension.java | 7 +- .../TrackerVisualizationDimensionTableInfo.kt | 86 ++++++++++++ .../TrackerVisualizationTableInfo.kt | 73 ++++++++++ .../core/visualization/VisualizationModule.kt | 2 + .../internal/TrackerVisualizationCall.kt | 79 +++++++++++ .../TrackerVisualizationCollectionCleaner.kt | 42 ++++++ ...alizationColumnsFiltersChildrenAppender.kt | 70 ++++++++++ .../TrackerVisualizationDimensionFields.kt | 54 ++++++++ .../TrackerVisualizationDimensionHandler.kt | 38 ++++++ ...rVisualizationDimensionRepetitionFields.kt | 45 ++++++ .../TrackerVisualizationDimensionStore.kt | 34 +++++ .../TrackerVisualizationDimensionStoreImpl.kt | 66 +++++++++ .../internal/TrackerVisualizationFields.kt | 62 +++++++++ .../internal/TrackerVisualizationHandler.kt | 68 ++++++++++ .../TrackerVisualizationModuleDownloader.kt | 45 ++++++ .../internal/TrackerVisualizationService.kt | 51 +++++++ .../internal/TrackerVisualizationStore.kt | 34 +++++ .../internal/TrackerVisualizationStoreImpl.kt | 65 +++++++++ .../internal/VisualizationModuleImpl.kt | 4 + .../internal/VisualizationModuleWiper.kt | 4 + .../TrackerVisualizationDimensionSamples.kt | 58 ++++++++ .../TrackerVisualizationSamples.kt | 73 ++++++++++ .../visualization/tracker_visualization.json | 74 ++++++++++ .../tracker_visualizations_1.json | 53 ++++++++ .../TrackerVisualizationShould.kt | 67 +++++++++ .../TrackerVisualizationHandlerShould.kt | 76 +++++++++++ 36 files changed, 1717 insertions(+), 10 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt create mode 100644 core/src/sharedTest/resources/visualization/tracker_visualization.json create mode 100644 core/src/sharedTest/resources/visualization/tracker_visualizations_1.json create mode 100644 core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt index 09efa1d909..b05fe2076b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt @@ -45,6 +45,7 @@ import org.hisp.dhis.android.core.usecase.stock.StockUseCase import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestEmptyDispatcher import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.Visualization import org.junit.After import org.junit.Test @@ -64,7 +65,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher testObserver.awaitTerminalEvent() - testObserver.assertValueCount(16) + testObserver.assertValueCount(17) val values = testObserver.values() @@ -87,6 +88,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher DataSet::class, Category::class, Visualization::class, + TrackerVisualization::class, ProgramIndicator::class, Indicator::class, LegendSet::class, diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt new file mode 100644 index 0000000000..87c78a25c1 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreIntegrationShould.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.data.database.LinkStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.visualization.TrackerVisualizationDimensionSamples +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class TrackerVisualizationDimensionStoreIntegrationShould : + LinkStoreAbstractIntegrationShould( + TrackerVisualizationDimensionStoreImpl(TestDatabaseAdapterFactory.get()), + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), + ) { + override fun addMasterUid(): String { + return "tracker_visualization_uid" + } + + override fun buildObject(): TrackerVisualizationDimension { + return TrackerVisualizationDimensionSamples.trackerVisualizationDimension() + } + + override fun buildObjectWithOtherMasterUid(): TrackerVisualizationDimension { + return TrackerVisualizationDimensionSamples.trackerVisualizationDimension().toBuilder() + .trackerVisualization("tracker_visualization_uid_2") + .build() + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt new file mode 100644 index 0000000000..7319a62014 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreIntegrationShould.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.data.database.IdentifiableObjectStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.visualization.TrackerVisualizationSamples +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class TrackerVisualizationStoreIntegrationShould : + IdentifiableObjectStoreAbstractIntegrationShould( + TrackerVisualizationStoreImpl(TestDatabaseAdapterFactory.get()), + TrackerVisualizationTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), + ) { + override fun buildObject(): TrackerVisualization { + return TrackerVisualizationSamples.trackerVisualization() + } + + override fun buildObjectToUpdate(): TrackerVisualization { + return TrackerVisualizationSamples.trackerVisualization().toBuilder() + .type(TrackerVisualizationType.LINE) + .build() + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt new file mode 100644 index 0000000000..f20ba449cd --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.testapp.visualization + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType +import org.hisp.dhis.android.core.visualization.TrackerVisualizationType +import org.junit.Test + +class TrackerVisualizationCollectionRepositoryMockIntegrationShould : + BaseMockIntegrationTestFullDispatcher() { + @Test + fun find_all() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_uids() { + val visualizationUids = d2.visualizationModule().trackerVisualizations() + .blockingGetUids() + + assertThat(visualizationUids.size).isEqualTo(1) + assertThat(visualizationUids.contains("s85urBIkN0z")).isTrue() + } + + @Test + fun find_by_description() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byDescription().eq("Child line list description") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_display_description() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byDisplayDescription().eq("Child line list description") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_type() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byType().eq(TrackerVisualizationType.LINE_LIST) + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_output_type() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byOutputType().eq(TrackerVisualizationOutputType.ENROLLMENT) + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_program() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byProgram().eq("IpHINAT79UW") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(1) + } + + @Test + fun find_by_program_stage() { + val visualizations = d2.visualizationModule().trackerVisualizations() + .byProgramStage().eq("IpHINAT79UW") + .blockingGet() + + assertThat(visualizations.size).isEqualTo(0) + } + + @Test + fun include_columns_and_filters_as_children() { + val visualization = d2.visualizationModule().trackerVisualizations() + .withColumnsAndFilters() + .uid("s85urBIkN0z") + .blockingGet()!! + + assertThat(visualization.columns()!!.size).isEqualTo(3) + assertThat(visualization.columns()!![0].dimension()).isEqualTo("ou") + assertThat(visualization.columns()!![0].dimensionType()).isEqualTo("ORGANISATION_UNIT") + assertThat(visualization.columns()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.columns()!![0].items()!![0].uid()).isEqualTo("USER_ORGUNIT") + + assertThat(visualization.filters()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].dimension()).isEqualTo("enrollmentDate") + assertThat(visualization.filters()!![0].dimensionType()).isEqualTo("PERIOD") + assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("LAST_10_YEARS") + } +} diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 3ac0fc841c..ad9e26c17c 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -130,3 +130,5 @@ CREATE TABLE DataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NO CREATE TABLE ProgramStageWorkingList (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, eventStatus TEXT, eventCreatedAt TEXT, eventOccurredAt TEXT, eventScheduledAt TEXT, enrollmentStatus TEXT, enrolledAt TEXT, enrollmentOccurredAt TEXT, orderProperty TEXT, displayColumnOrder TEXT, orgUnit TEXT, ouMode TEXT, assignedUserMode TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (orgUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadURL TEXT, version TEXT); CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt index 019ce788df..88afbf8e25 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt @@ -47,6 +47,7 @@ import org.hisp.dhis.android.core.usecase.stock.internal.StockUseCaseService import org.hisp.dhis.android.core.user.internal.AuthorityService import org.hisp.dhis.android.core.user.internal.UserService import org.hisp.dhis.android.core.validation.internal.ValidationRuleService +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationService import org.hisp.dhis.android.core.visualization.internal.VisualizationService import org.koin.dsl.module import retrofit2.Retrofit @@ -97,6 +98,7 @@ internal val servicesDIModule = module { single { get().create(TrackedEntityTypeService::class.java) } single { get().create(TrackerExporterService::class.java) } single { get().create(TrackerImporterService::class.java) } + single { get().create(TrackerVisualizationService::class.java) } single { get().create(UserService::class.java) } single { get().create(ValidationRuleService::class.java) } single { get().create(VisualizationService::class.java) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt index d5e7f5aff9..b2737fcb56 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/identifiable/internal/ObjectWithUidListColumnAdapter.kt @@ -27,22 +27,44 @@ */ package org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal -import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.JSONObjectListColumnAdapter +import android.content.ContentValues +import android.database.Cursor +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.JsonMappingException +import com.gabrielittner.auto.value.cursor.ColumnTypeAdapter import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory import org.hisp.dhis.android.core.common.ObjectWithUid -internal class ObjectWithUidListColumnAdapter : JSONObjectListColumnAdapter() { - override fun getObjectClass(): Class> { - return ArrayList().javaClass +internal class ObjectWithUidListColumnAdapter : ColumnTypeAdapter> { + + override fun fromCursor(cursor: Cursor, columnName: String): List { + val columnIndex = cursor.getColumnIndex(columnName) + val str = cursor.getString(columnIndex) + return try { + val idList = ObjectMapperFactory.objectMapper().readValue(str, ArrayList().javaClass) + idList.map { ObjectWithUid.create(it) } + } catch (e: JsonProcessingException) { + listOf() + } catch (e: JsonMappingException) { + listOf() + } catch (e: IllegalArgumentException) { + listOf() + } catch (e: IllegalStateException) { + listOf() + } } - override fun serialize(o: List?): String? { - return ObjectWithUidListColumnAdapter.serialize(o) + override fun toContentValues(values: ContentValues, columnName: String, value: List?) { + try { + values.put(columnName, serialize(value)) + } catch (e: JsonProcessingException) { + e.printStackTrace() + } } companion object { fun serialize(o: List?): String? { - return o?.let { + return o?.map { it.uid() }.let { ObjectMapperFactory.objectMapper().writeValueAsString(it) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt index ebb68b02e7..a24e4cb19e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt @@ -69,7 +69,9 @@ import org.hisp.dhis.android.core.usecase.UseCaseModuleDownloader import org.hisp.dhis.android.core.usecase.stock.StockUseCase import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.internal.UserModuleDownloader +import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.Visualization +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationModuleDownloader import org.hisp.dhis.android.core.visualization.internal.VisualizationModuleDownloader import org.koin.core.annotation.Singleton @@ -86,6 +88,7 @@ internal class MetadataCall( private val organisationUnitModuleDownloader: OrganisationUnitModuleDownloader, private val dataSetDownloader: DataSetModuleDownloader, private val visualizationDownloader: VisualizationModuleDownloader, + private val trackerVisualizationDownloader: TrackerVisualizationModuleDownloader, private val constantModuleDownloader: ConstantModuleDownloader, private val indicatorModuleDownloader: IndicatorModuleDownloader, private val programIndicatorModuleDownloader: ProgramIndicatorModuleDownloader, @@ -100,7 +103,7 @@ internal class MetadataCall( ) { companion object { - const val CALLS_COUNT = 15 + const val CALLS_COUNT = 16 } @Suppress("TooGenericExceptionCaught") @@ -159,6 +162,9 @@ internal class MetadataCall( visualizationDownloader.downloadMetadata() emit(progressManager.increaseProgress(Visualization::class.java, false)) + trackerVisualizationDownloader.downloadMetadata() + emit(progressManager.increaseProgress(TrackerVisualization::class.java, false)) + programIndicatorModuleDownloader.downloadMetadata() emit(progressManager.increaseProgress(ProgramIndicator::class.java, false)) diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index b86b31288b..62e48eff2f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -95,6 +95,7 @@ public class Dhis2MockServer { private static final String CATEGORY_OPTION_ORGUNITS_JSON = "category/category_option_orgunits.json"; private static final String VISUALIZATIONS_1_JSON = "visualization/visualizations_1.json"; private static final String VISUALIZATIONS_2_JSON = "visualization/visualizations_2.json"; + private static final String TRACKER_VISUALIZATIONS_1_JSON = "visualization/tracker_visualizations_1.json"; private static final String ORGANISATION_UNIT_LEVELS_JSON = "organisationunit/organisation_unit_levels.json"; private static final String CONSTANTS_JSON = "constant/constants.json"; private static final String USER_JSON = "user/user38.json"; @@ -274,6 +275,8 @@ public MockResponse dispatch(RecordedRequest request) { return createMockResponse(VISUALIZATIONS_1_JSON); } else if (path.startsWith("/api/visualizations/FAFa11yFeFe?")) { return createMockResponse(VISUALIZATIONS_2_JSON); + } else if (path.startsWith("/api/eventVisualizations/s85urBIkN0z?")) { + return createMockResponse(TRACKER_VISUALIZATIONS_1_JSON); } else if (path.startsWith("/api/organisationUnits?")) { return createMockResponse(ORGANISATION_UNITS_JSON); } else if (path.startsWith("/api/organisationUnitLevels?")) { @@ -374,6 +377,7 @@ public void enqueueMetadataResponses() { enqueueMockResponse(CATEGORY_OPTION_ORGUNITS_JSON); enqueueMockResponse(VISUALIZATIONS_1_JSON); enqueueMockResponse(VISUALIZATIONS_2_JSON); + enqueueMockResponse(TRACKER_VISUALIZATIONS_1_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); enqueueMockResponse(INDICATORS_JSON); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt new file mode 100644 index 0000000000..21fa8c6bc6 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationCollectionRepository.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.arch.repositories.collection.internal.ReadOnlyIdentifiableCollectionRepositoryImpl +import org.hisp.dhis.android.core.arch.repositories.filters.internal.EnumFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.FilterConnectorFactory +import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationColumnsFiltersChildrenAppender +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationFields +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationStore +import org.hisp.dhis.android.core.visualization.internal.VisualizationFields +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("TooManyFunctions") +class TrackerVisualizationCollectionRepository internal constructor( + store: TrackerVisualizationStore, + databaseAdapter: DatabaseAdapter, + scope: RepositoryScope, +) : ReadOnlyIdentifiableCollectionRepositoryImpl( + store, + databaseAdapter, + childrenAppenders, + scope, + FilterConnectorFactory( + scope, + ) { s: RepositoryScope -> + TrackerVisualizationCollectionRepository( + store, + databaseAdapter, + s, + ) + }, +) { + fun byDescription(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.DESCRIPTION) + } + + fun byDisplayDescription(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.DISPLAY_DESCRIPTION) + } + + fun byType(): EnumFilterConnector { + return cf.enumC(TrackerVisualizationTableInfo.Columns.TYPE) + } + + fun byOutputType(): EnumFilterConnector { + return cf.enumC(TrackerVisualizationTableInfo.Columns.OUTPUT_TYPE) + } + + fun byProgram(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.PROGRAM) + } + + fun byProgramStage(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.PROGRAM_STAGE) + } + + fun byTrackedEntityType(): StringFilterConnector { + return cf.string(TrackerVisualizationTableInfo.Columns.TRACKED_ENTITY_TYPE) + } + fun withColumnsAndFilters(): TrackerVisualizationCollectionRepository { + return cf.withChild(VisualizationFields.ITEMS) + } + + internal companion object { + val childrenAppenders: ChildrenAppenderGetter = mapOf( + TrackerVisualizationFields.ITEMS to TrackerVisualizationColumnsFiltersChildrenAppender::create, + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java index f7641c6dbf..7557ca128b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimension.java @@ -28,6 +28,8 @@ package org.hisp.dhis.android.core.visualization; +import android.database.Cursor; + import androidx.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonProperty; @@ -36,7 +38,6 @@ import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; -import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.StringListColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.LayoutPositionColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; @@ -94,6 +95,10 @@ public static Builder builder() { return new AutoValue_TrackerVisualizationDimension.Builder(); } + public static TrackerVisualizationDimension create(Cursor cursor) { + return $AutoValue_TrackerVisualizationDimension.createFromCursor(cursor); + } + public abstract Builder toBuilder(); @AutoValue.Builder diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt new file mode 100644 index 0000000000..f1172a1261 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationDimensionTableInfo.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns + +object TrackerVisualizationDimensionTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "TrackerVisualizationDimension" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : CoreColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + TRACKER_VISUALIZATION, + POSITION, + DIMENSION, + DIMENSION_TYPE, + PROGRAM, + PROGRAM_STAGE, + ITEMS, + FILTER, + REPETITION, + ) + } + + override fun whereUpdate(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + TRACKER_VISUALIZATION, + POSITION, + DIMENSION, + DIMENSION_TYPE, + ) + } + + companion object { + const val TRACKER_VISUALIZATION = "trackerVisualization" + const val POSITION = "position" + const val DIMENSION = "dimension" + const val DIMENSION_TYPE = "dimensionType" + const val PROGRAM = "program" + const val PROGRAM_STAGE = "programStage" + const val ITEMS = "items" + const val FILTER = "filter" + const val REPETITION = "repetition" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt new file mode 100644 index 0000000000..f80ffef08b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationTableInfo.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns +import org.hisp.dhis.android.core.common.IdentifiableColumns + +object TrackerVisualizationTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "TrackerVisualization" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : IdentifiableColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + DESCRIPTION, + DISPLAY_DESCRIPTION, + TYPE, + OUTPUT_TYPE, + PROGRAM, + PROGRAM_STAGE, + TRACKED_ENTITY_TYPE, + ) + } + + companion object { + const val DESCRIPTION = "description" + const val DISPLAY_DESCRIPTION = "displayDescription" + const val TYPE = "type" + const val OUTPUT_TYPE = "outputType" + const val PROGRAM = "program" + const val PROGRAM_STAGE = "programStage" + const val TRACKED_ENTITY_TYPE = "trackedEntityType" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt index 06627c886b..1a70429432 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/VisualizationModule.kt @@ -29,4 +29,6 @@ package org.hisp.dhis.android.core.visualization interface VisualizationModule { fun visualizations(): VisualizationCollectionRepository + + fun trackerVisualizations(): TrackerVisualizationCollectionRepository } diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt new file mode 100644 index 0000000000..ae4855d602 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.arch.call.factories.internal.UidsCallCoroutines +import org.hisp.dhis.android.core.common.internal.AccessFields +import org.hisp.dhis.android.core.systeminfo.DHISVersion +import org.hisp.dhis.android.core.systeminfo.DHISVersionManager +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationCall( + private val handler: TrackerVisualizationHandler, + private val service: TrackerVisualizationService, + private val dhis2VersionManager: DHISVersionManager, + private val apiDownloader: APIDownloader, +) : UidsCallCoroutines { + + companion object { + // Workaround for DHIS2-16746. Force queries to entity endpoint instead of list endpoint. + private const val MAX_UID_LIST_SIZE = 1 + } + + override suspend fun download(uids: Set): List { + val accessFilter = "access." + AccessFields.read.eq(true).generateString() + + // TODO Limit by version + + return if (dhis2VersionManager.isGreaterOrEqualThan(DHISVersion.V2_38)) { + apiDownloader.downloadPartitioned( + uids, + MAX_UID_LIST_SIZE, + handler, + ) { partitionUids: Set -> + try { + val visualization = service.getSingleTrackerVisualization( + partitionUids.first(), + TrackerVisualizationFields.allFields, + accessFilter = accessFilter, + paging = false, + ) + Payload(listOf(visualization)) + } catch (ignored: Exception) { + Payload() + } + } + } else { + emptyList() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt new file mode 100644 index 0000000000..f9ce80f3c6 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCollectionCleaner.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.cleaners.internal.CollectionCleanerImpl +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationCollectionCleaner( + databaseAdapter: DatabaseAdapter, +) : CollectionCleanerImpl( + tableName = TrackerVisualizationTableInfo.TABLE_INFO.name(), + databaseAdapter = databaseAdapter, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt new file mode 100644 index 0000000000..f9b0590e61 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationColumnsFiltersChildrenAppender.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.internal.SingleParentChildStore +import org.hisp.dhis.android.core.arch.db.stores.internal.StoreFactory.singleParentChildStore +import org.hisp.dhis.android.core.arch.db.stores.projections.internal.SingleParentChildProjection +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppender +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo + +internal class TrackerVisualizationColumnsFiltersChildrenAppender private constructor( + private val linkChildStore: SingleParentChildStore, +) : ChildrenAppender() { + override fun appendChildren(m: TrackerVisualization): TrackerVisualization { + val items = linkChildStore.getChildren(m) + val groupedByPosition = items + .groupBy { it.position() } + + return m.toBuilder() + .columns(groupedByPosition[LayoutPosition.COLUMN] ?: emptyList()) + .filters(groupedByPosition[LayoutPosition.FILTER] ?: emptyList()) + .build() + } + + companion object { + private val CHILD_PROJECTION = SingleParentChildProjection( + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.Columns.TRACKER_VISUALIZATION, + ) + + fun create(databaseAdapter: DatabaseAdapter): ChildrenAppender { + return TrackerVisualizationColumnsFiltersChildrenAppender( + singleParentChildStore( + databaseAdapter, + CHILD_PROJECTION, + ) { cursor: Cursor -> TrackerVisualizationDimension.create(cursor) }, + ) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt new file mode 100644 index 0000000000..512eca9515 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionFields.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo + +internal object TrackerVisualizationDimensionFields { + private val fh = FieldsHelper() + + val allFields: Fields = + Fields.builder() + .fields( + fh.field(TrackerVisualizationDimensionTableInfo.Columns.DIMENSION), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.DIMENSION_TYPE), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.PROGRAM), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.PROGRAM_STAGE), + fh.nestedFieldWithUid(TrackerVisualizationDimensionTableInfo.Columns.ITEMS), + fh.field(TrackerVisualizationDimensionTableInfo.Columns.FILTER), + fh.nestedField( + TrackerVisualizationDimensionTableInfo.Columns.REPETITION, + ) + .with(TrackerVisualizationDimensionRepetitionFields.allFields), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt new file mode 100644 index 0000000000..cc8d979962 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.LinkHandlerImpl +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationDimensionHandler( + store: TrackerVisualizationDimensionStore, +) : LinkHandlerImpl(store) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt new file mode 100644 index 0000000000..e1cd496319 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionRepetitionFields.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +internal object TrackerVisualizationDimensionRepetitionFields { + private const val INDEXES = "indexes" + + private val fh = FieldsHelper() + + val allFields: Fields = + Fields.builder() + .fields( + fh.field(INDEXES), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt new file mode 100644 index 0000000000..3f3ae075b2 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.LinkStore +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension + +internal interface TrackerVisualizationDimensionStore : LinkStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt new file mode 100644 index 0000000000..d3a329a089 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationDimensionStoreImpl.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.adapters.custom.internal.TrackerVisualizationDimensionRepetitionColumnAdapter +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidListColumnAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper +import org.hisp.dhis.android.core.arch.db.stores.internal.LinkStoreImpl +import org.hisp.dhis.android.core.arch.helpers.UidsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("MagicNumber") +internal class TrackerVisualizationDimensionStoreImpl( + databaseAdapter: DatabaseAdapter, +) : TrackerVisualizationDimensionStore, + LinkStoreImpl( + databaseAdapter, + TrackerVisualizationDimensionTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.Columns.TRACKER_VISUALIZATION, + BINDER, + { TrackerVisualizationDimension.create(it) }, + ) { + companion object { + private val BINDER = StatementBinder { o: TrackerVisualizationDimension, w: StatementWrapper -> + w.bind(1, o.trackerVisualization()) + w.bind(2, o.position()) + w.bind(3, o.dimension()) + w.bind(4, o.dimensionType()) + w.bind(5, UidsHelper.getUidOrNull(o.program())) + w.bind(6, UidsHelper.getUidOrNull(o.programStage())) + w.bind(7, ObjectWithUidListColumnAdapter.serialize(o.items())) + w.bind(8, o.filter()) + w.bind(9, TrackerVisualizationDimensionRepetitionColumnAdapter.serialize(o.repetition())) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt new file mode 100644 index 0000000000..c66c6a55b5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationFields.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo + +internal object TrackerVisualizationFields { + private const val COLUMNS = "columns" + private const val FILTERS = "filters" + + internal const val ITEMS = "items" + + private val fh = FieldsHelper() + val uid = fh.uid() + + val allFields: Fields = + Fields.builder() + .fields(fh.getIdentifiableFields()) + .fields( + fh.field(TrackerVisualizationTableInfo.Columns.DESCRIPTION), + fh.field(TrackerVisualizationTableInfo.Columns.DISPLAY_DESCRIPTION), + fh.field(TrackerVisualizationTableInfo.Columns.TYPE), + fh.field(TrackerVisualizationTableInfo.Columns.OUTPUT_TYPE), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.PROGRAM), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.PROGRAM_STAGE), + fh.nestedFieldWithUid(TrackerVisualizationTableInfo.Columns.TRACKED_ENTITY_TYPE), + fh.nestedField(COLUMNS) + .with(TrackerVisualizationDimensionFields.allFields), + fh.nestedField(FILTERS) + .with(TrackerVisualizationDimensionFields.allFields), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt new file mode 100644 index 0000000000..ad25800f2c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationHandler( + store: TrackerVisualizationStore, + private val trackerVisualizationCollectionCleaner: TrackerVisualizationCollectionCleaner, + private val dimensionHandler: TrackerVisualizationDimensionHandler, +) : IdentifiableHandlerImpl(store) { + + override fun afterObjectHandled(o: TrackerVisualization, action: HandleAction) { + val dimensions = + toDimensions(o.columns(), LayoutPosition.COLUMN) + + toDimensions(o.filters(), LayoutPosition.FILTER) + + dimensionHandler.handleMany(o.uid(), dimensions) { + it.toBuilder().trackerVisualization(o.uid()).build() + } + } + + override fun afterCollectionHandled(oCollection: Collection?) { + trackerVisualizationCollectionCleaner.deleteNotPresent(oCollection) + } + + private fun toDimensions( + dimensions: List?, + position: LayoutPosition, + ): List { + return dimensions?.map { dimension -> + dimension.toBuilder() + .position(position) + .build() + } ?: emptyList() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt new file mode 100644 index 0000000000..f419ddfc09 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.modules.internal.TypedModuleDownloader +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationModuleDownloader internal constructor( + private val visualizationCall: TrackerVisualizationCall, +) : + TypedModuleDownloader> { + + override suspend fun downloadMetadata(): List { + // TODO Extract tracker visualization uids from analytics settings + val trackerVisualizations = setOf("s85urBIkN0z") + return visualizationCall.download(trackerVisualizations) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt new file mode 100644 index 0000000000..72caa546f0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationService.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +internal interface TrackerVisualizationService { + + @GET("$TRACKER_VISUALIZATIONS/{$TRACKER_VISUALIZATION_UID}") + suspend fun getSingleTrackerVisualization( + @Path(TRACKER_VISUALIZATION_UID) uid: String, + @Query("fields") @Which fields: Fields, + @Query("filter") accessFilter: String, + @Query("paging") paging: Boolean, + ): TrackerVisualization + + companion object { + const val TRACKER_VISUALIZATIONS = "eventVisualizations" + const val TRACKER_VISUALIZATION_UID = "visualizationUid" + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt new file mode 100644 index 0000000000..faa084da03 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.visualization.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore +import org.hisp.dhis.android.core.visualization.TrackerVisualization + +internal interface TrackerVisualizationStore : IdentifiableObjectStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt new file mode 100644 index 0000000000..a0dca332db --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationStoreImpl.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.IdentifiableStatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStoreImpl +import org.hisp.dhis.android.core.arch.helpers.UidsHelper +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("MagicNumber") +internal class TrackerVisualizationStoreImpl( + databaseAdapter: DatabaseAdapter, +) : TrackerVisualizationStore, + IdentifiableObjectStoreImpl( + databaseAdapter, + TrackerVisualizationTableInfo.TABLE_INFO, + BINDER, + { cursor: Cursor -> TrackerVisualization.create(cursor) }, + ) { + companion object { + private val BINDER = object : IdentifiableStatementBinder() { + override fun bindToStatement(o: TrackerVisualization, w: StatementWrapper) { + super.bindToStatement(o, w) + w.bind(7, o.description()) + w.bind(8, o.displayDescription()) + w.bind(9, o.type()) + w.bind(10, o.outputType()) + w.bind(11, UidsHelper.getUidOrNull(o.program())) + w.bind(12, UidsHelper.getUidOrNull(o.programStage())) + w.bind(13, UidsHelper.getUidOrNull(o.trackedEntityType())) + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt index 6971804aef..470ac76ea6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleImpl.kt @@ -27,6 +27,7 @@ */ package org.hisp.dhis.android.core.visualization.internal +import org.hisp.dhis.android.core.visualization.TrackerVisualizationCollectionRepository import org.hisp.dhis.android.core.visualization.VisualizationCollectionRepository import org.hisp.dhis.android.core.visualization.VisualizationModule import org.koin.core.annotation.Singleton @@ -34,7 +35,10 @@ import org.koin.core.annotation.Singleton @Singleton internal class VisualizationModuleImpl( private val visualizations: VisualizationCollectionRepository, + private val trackerVisualizations: TrackerVisualizationCollectionRepository, ) : VisualizationModule { override fun visualizations(): VisualizationCollectionRepository = visualizations + + override fun trackerVisualizations(): TrackerVisualizationCollectionRepository = trackerVisualizations } diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt index c4f3801fed..79df93bd42 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleWiper.kt @@ -27,6 +27,8 @@ */ package org.hisp.dhis.android.core.visualization.internal +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionTableInfo +import org.hisp.dhis.android.core.visualization.TrackerVisualizationTableInfo import org.hisp.dhis.android.core.visualization.VisualizationDimensionItemTableInfo import org.hisp.dhis.android.core.visualization.VisualizationTableInfo import org.hisp.dhis.android.core.wipe.internal.ModuleWiper @@ -37,6 +39,8 @@ import org.koin.core.annotation.Singleton class VisualizationModuleWiper internal constructor(private val tableWiper: TableWiper) : ModuleWiper { override fun wipeMetadata() { tableWiper.wipeTables( + TrackerVisualizationTableInfo.TABLE_INFO, + TrackerVisualizationDimensionTableInfo.TABLE_INFO, VisualizationTableInfo.TABLE_INFO, VisualizationDimensionItemTableInfo.TABLE_INFO, ) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt new file mode 100644 index 0000000000..a402efdd7c --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationDimensionSamples.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.data.visualization + +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.visualization.LayoutPosition +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimensionRepetition + +object TrackerVisualizationDimensionSamples { + + fun trackerVisualizationDimension(): TrackerVisualizationDimension = + TrackerVisualizationDimension.builder() + .id(1L) + .trackerVisualization("tracker_visualization_uid") + .position(LayoutPosition.COLUMN) + .dimension("ou") + .dimensionType("ORGANISATION_UNIT") + .program(ObjectWithUid.create("program_uid")) + .programStage(ObjectWithUid.create("program_stage_uid")) + .items( + listOf( + ObjectWithUid.create("USER_ORGUNIT"), + ObjectWithUid.create("USER_ORGUNIT_CHILDREN"), + ), + ) + .repetition( + TrackerVisualizationDimensionRepetition.builder() + .indexes(listOf(-1, 1, 0)) + .build(), + ) + .build() +} diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt new file mode 100644 index 0000000000..9f9e50206e --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/visualization/TrackerVisualizationSamples.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.data.visualization + +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.data.utils.FillPropertiesTestUtils +import org.hisp.dhis.android.core.visualization.* + +internal object TrackerVisualizationSamples { + + private const val DATE_STR = "2021-06-16T14:26:50.195" + private val DATE = FillPropertiesTestUtils.parseDate(DATE_STR) + + @JvmStatic + fun trackerVisualization(): TrackerVisualization = TrackerVisualization.builder() + .id(1L) + .uid("PYBH8ZaAQnC") + .name("Android SDK Tracker Visualization sample") + .displayName("Android SDK Tracker Visualization sample") + .created(DATE) + .lastUpdated(DATE) + .description("Sample tracker visualization for the Android SDK") + .displayDescription("Sample tracker visualization for the Android SDK") + .type(TrackerVisualizationType.LINE_LIST) + .outputType(TrackerVisualizationOutputType.ENROLLMENT) + .program(ObjectWithUid.create("")) + .programStage(ObjectWithUid.create("")) + .trackedEntityType(ObjectWithUid.create("")) + .columns( + listOf( + TrackerVisualizationDimension.builder() + .dimension("ou") + .dimensionType("ORGANISATION_UNIT") + .items(listOf(ObjectWithUid.create("USER_ORGUNIT"))) + .build(), + ), + ) + .filters( + listOf( + TrackerVisualizationDimension.builder() + .dimension("enrollmentDate") + .dimensionType("PERIOD") + .items(listOf(ObjectWithUid.create("LAST_5_YEARS"))) + .build(), + ), + ) + .build() +} diff --git a/core/src/sharedTest/resources/visualization/tracker_visualization.json b/core/src/sharedTest/resources/visualization/tracker_visualization.json new file mode 100644 index 0000000000..8593ea5dba --- /dev/null +++ b/core/src/sharedTest/resources/visualization/tracker_visualization.json @@ -0,0 +1,74 @@ +{ + "name": "TB program", + "created": "2024-02-07T07:37:41.116", + "lastUpdated": "2024-02-07T07:43:25.556", + "description": "Line list for TB program", + "type": "LINE_LIST", + "program": { + "id": "ur1Edk5Oe2n" + }, + "outputType": "ENROLLMENT", + "filters": [ + { + "dimensionType": "PERIOD", + "items": [ + { + "id": "LAST_5_YEARS" + } + ], + "dimension": "enrollmentDate" + } + ], + "columns": [ + { + "dimensionType": "ORGANISATION_UNIT", + "items": [ + { + "id": "USER_ORGUNIT" + } + ], + "dimension": "ou" + }, + { + "dimensionType": "PROGRAM_ATTRIBUTE", + "items": [], + "dimension": "w75KJ2mc4zz" + }, + { + "dimensionType": "DATA_X", + "items": [ + { + "id": "COMPLETED" + }, + { + "id": "ACTIVE" + } + ], + "dimension": "programStatus" + }, + { + "dimensionType": "PROGRAM_DATA_ELEMENT", + "items": [], + "programStage": { + "id": "EPEcjy3FWmI" + }, + "program": { + "id": "ur1Edk5Oe2n" + }, + "filter": "IN:1", + "dimension": "fCXKBdc27Bt", + "repetition": { + "indexes": [ + 1, + 2, + -2, + -1, + 0 + ] + } + } + ], + "displayName": "TB program", + "displayDescription": "Line list for TB program", + "id": "s85urBIkN0z" +} diff --git a/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json new file mode 100644 index 0000000000..9b0898d440 --- /dev/null +++ b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json @@ -0,0 +1,53 @@ +{ + "name": "Child line list", + "created": "2024-02-07T07:37:41.116", + "lastUpdated": "2024-02-07T07:43:25.556", + "description": "Child line list description", + "type": "LINE_LIST", + "program": { + "id": "IpHINAT79UW" + }, + "outputType": "ENROLLMENT", + "filters": [ + { + "dimensionType": "PERIOD", + "items": [ + { + "id": "LAST_10_YEARS" + } + ], + "dimension": "enrollmentDate" + } + ], + "columns": [ + { + "dimensionType": "ORGANISATION_UNIT", + "items": [ + { + "id": "USER_ORGUNIT" + } + ], + "dimension": "ou" + }, + { + "dimensionType": "PROGRAM_ATTRIBUTE", + "items": [], + "dimension": "cejWyOfXge6" + }, + { + "dimensionType": "DATA_X", + "items": [ + { + "id": "COMPLETED" + }, + { + "id": "ACTIVE" + } + ], + "dimension": "programStatus" + } + ], + "displayName": "Child line list", + "displayDescription": "Child line list description", + "id": "s85urBIkN0z" +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt new file mode 100644 index 0000000000..b2b45d9226 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/TrackerVisualizationShould.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class TrackerVisualizationShould : BaseObjectShould("visualization/tracker_visualization.json"), ObjectShould { + + @Test + override fun map_from_json_string() { + val visualization = objectMapper.readValue(jsonStream, TrackerVisualization::class.java) + + assertThat(visualization.uid()).isEqualTo("s85urBIkN0z") + assertThat(visualization.name()).isEqualTo("TB program") + assertThat(visualization.displayName()).isEqualTo("TB program") + assertThat(visualization.description()).isEqualTo("Line list for TB program") + assertThat(visualization.displayDescription()).isEqualTo("Line list for TB program") + assertThat(visualization.created()).isEqualTo(DateUtils.DATE_FORMAT.parse("2024-02-07T07:37:41.116")) + assertThat(visualization.lastUpdated()).isEqualTo(DateUtils.DATE_FORMAT.parse("2024-02-07T07:43:25.556")) + assertThat(visualization.type()).isEqualTo(TrackerVisualizationType.LINE_LIST) + assertThat(visualization.outputType()).isEqualTo(TrackerVisualizationOutputType.ENROLLMENT) + assertThat(visualization.program()!!.uid()).isEqualTo("ur1Edk5Oe2n") + + assertThat(visualization.columns()?.size).isEqualTo(4) + val dataElementColumn = visualization.columns()!!.find { it.dimensionType() == "PROGRAM_DATA_ELEMENT" }!! + assertThat(dataElementColumn.dimension()).isEqualTo("fCXKBdc27Bt") + assertThat(dataElementColumn.program()!!.uid()).isEqualTo("ur1Edk5Oe2n") + assertThat(dataElementColumn.programStage()!!.uid()).isEqualTo("EPEcjy3FWmI") + assertThat(dataElementColumn.filter()).isEqualTo("IN:1") + assertThat(dataElementColumn.repetition()!!.indexes()).isEqualTo(listOf(1, 2, -2, -1, 0)) + + assertThat(visualization.filters()?.size).isEqualTo(1) + assertThat(visualization.filters()!![0].dimension()).isEqualTo("enrollmentDate") + assertThat(visualization.filters()!![0].dimensionType()).isEqualTo("PERIOD") + assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(1) + assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("LAST_5_YEARS") + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt new file mode 100644 index 0000000000..4690a890fb --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.visualization.internal + +import com.nhaarman.mockitokotlin2.* +import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class TrackerVisualizationHandlerShould { + + private val store: TrackerVisualizationStore = mock() + private val collectionCleaner: TrackerVisualizationCollectionCleaner = mock() + private val dimensionHandler: TrackerVisualizationDimensionHandler = mock() + private val dimension: TrackerVisualizationDimension = TrackerVisualizationDimension.builder().build() + private val trackerVisualization: TrackerVisualization = mock() + + // object to test + private lateinit var trackerVisualizationHandler: TrackerVisualizationHandler + + @Before + fun setUp() { + trackerVisualizationHandler = TrackerVisualizationHandler( + store, + collectionCleaner, + dimensionHandler, + ) + + whenever(trackerVisualization.columns()).doReturn(listOf(dimension)) + whenever(trackerVisualization.filters()).doReturn(listOf(dimension)) + whenever(store.updateOrInsert(any())).doReturn(HandleAction.Insert) + whenever(trackerVisualization.uid()).doReturn("tracker_visualization_uid") + } + + @Test + fun call_items_handler() { + trackerVisualizationHandler.handleMany(listOf(trackerVisualization)) + verify(dimensionHandler).handleMany(any(), any(), any()) + } + + @Test + fun call_collection_cleaner() { + trackerVisualizationHandler.handleMany(listOf(trackerVisualization)) + verify(collectionCleaner).deleteNotPresent(any()) + } +} From 68135be5ca07a37e4e3d30f245b8bd29df18a02f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 12:39:47 +0100 Subject: [PATCH 071/222] [ANDROSDK-1810] Add additional foreign key constraints --- core/src/main/assets/snapshots/snapshot.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index ad9e26c17c..bf725ddfd3 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -130,5 +130,5 @@ CREATE TABLE DataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, namespace TEXT NO CREATE TABLE ProgramStageWorkingList (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, eventStatus TEXT, eventCreatedAt TEXT, eventOccurredAt TEXT, eventScheduledAt TEXT, enrollmentStatus TEXT, enrolledAt TEXT, enrollmentOccurredAt TEXT, orderProperty TEXT, displayColumnOrder TEXT, orgUnit TEXT, ouMode TEXT, assignedUserMode TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (orgUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadURL TEXT, version TEXT); CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); -CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT); -CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); From 51ea34b3ff7c633d71d5b845b511ba8f058565a2 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 14:05:01 +0100 Subject: [PATCH 072/222] [ANDROSDK-1810] Adapt unit test --- .../dhis/android/core/domain/metadata/MetadataCallShould.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt index 8061a1f6c4..6e63293cd0 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt @@ -60,6 +60,7 @@ import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoModuleDownloader import org.hisp.dhis.android.core.usecase.UseCaseModuleDownloader import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.internal.UserModuleDownloader +import org.hisp.dhis.android.core.visualization.internal.TrackerVisualizationModuleDownloader import org.hisp.dhis.android.core.visualization.internal.VisualizationModuleDownloader import org.junit.Assert.fail import org.junit.Before @@ -81,6 +82,7 @@ class MetadataCallShould : BaseCallShould() { private val organisationUnitDownloader: OrganisationUnitModuleDownloader = mock() private val dataSetDownloader: DataSetModuleDownloader = mock() private val visualizationDownloader: VisualizationModuleDownloader = mock() + private val trackerVisualizationDownloader: TrackerVisualizationModuleDownloader = mock() private val constantDownloader: ConstantModuleDownloader = mock() private val indicatorDownloader: IndicatorModuleDownloader = mock() private val programIndicatorModuleDownloader: ProgramIndicatorModuleDownloader = mock() @@ -136,6 +138,9 @@ class MetadataCallShould : BaseCallShould() { visualizationDownloader.stub { onBlocking { downloadMetadata() }.doReturn(emptyList()) } + trackerVisualizationDownloader.stub { + onBlocking { downloadMetadata() }.doReturn(emptyList()) + } legendSetModuleDownloader.stub { onBlocking { downloadMetadata() }.doReturn(Unit) } @@ -173,6 +178,7 @@ class MetadataCallShould : BaseCallShould() { organisationUnitDownloader, dataSetDownloader, visualizationDownloader, + trackerVisualizationDownloader, constantDownloader, indicatorDownloader, programIndicatorModuleDownloader, From cf633eb1846ab84af72423c4003f38533de93620 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 7 Feb 2024 15:12:35 +0100 Subject: [PATCH 073/222] [ANDROSDK-1810] Fix sonar code smells --- .../dhis/android/core/visualization/TrackerVisualization.java | 2 +- .../core/visualization/internal/TrackerVisualizationCall.kt | 2 -- .../internal/TrackerVisualizationModuleDownloader.kt | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java index d7be657254..1b0b416c8a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/TrackerVisualization.java @@ -100,7 +100,7 @@ public static Builder builder() { } public static TrackerVisualization create(Cursor cursor) { - return AutoValue_TrackerVisualization.createFromCursor(cursor); + return $AutoValue_TrackerVisualization.createFromCursor(cursor); } public abstract Builder toBuilder(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt index ae4855d602..a6c7ae9101 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationCall.kt @@ -52,8 +52,6 @@ internal class TrackerVisualizationCall( override suspend fun download(uids: Set): List { val accessFilter = "access." + AccessFields.read.eq(true).generateString() - // TODO Limit by version - return if (dhis2VersionManager.isGreaterOrEqualThan(DHISVersion.V2_38)) { apiDownloader.downloadPartitioned( uids, diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt index f419ddfc09..5fb6459119 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt @@ -38,7 +38,7 @@ internal class TrackerVisualizationModuleDownloader internal constructor( TypedModuleDownloader> { override suspend fun downloadMetadata(): List { - // TODO Extract tracker visualization uids from analytics settings + // Extract visualizations in ANDROSDK-1811 val trackerVisualizations = setOf("s85urBIkN0z") return visualizationCall.download(trackerVisualizations) } From c7d19f9038d079e2afdddcdf2796821b0fb5a634 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 12 Feb 2024 10:16:30 +0100 Subject: [PATCH 074/222] [ANDROSDK-1800] Update db version --- core/src/main/assets/migrations/{158.sql => 159.sql} | 0 .../core/arch/db/access/internal/BaseDatabaseOpenHelper.kt | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename core/src/main/assets/migrations/{158.sql => 159.sql} (100%) diff --git a/core/src/main/assets/migrations/158.sql b/core/src/main/assets/migrations/159.sql similarity index 100% rename from core/src/main/assets/migrations/158.sql rename to core/src/main/assets/migrations/159.sql diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index 604c32b648..ddda39d45e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 158 + const val VERSION = 159 } } From 2b151011deb625aa74ba319ffc429fd6f2d1631c Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 13 Feb 2024 08:50:03 +0100 Subject: [PATCH 075/222] [ANDROSDK-1800] Add nullable values --- .../org/hisp/dhis/android/core/map/layer/MapLayer.java | 8 ++++---- .../map/layer/internal/externalmap/ExternalMapLayer.kt | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java index 2b2b723118..0d89ada03b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/MapLayer.java @@ -85,18 +85,18 @@ public abstract class MapLayer extends BaseObject implements ObjectWithUidInterf @ColumnAdapter(IgnoreMapLayerImageryProviderColumnAdapter.class) public abstract List imageryProviders(); - @NonNull + @Nullable public abstract String code(); - @NonNull + @Nullable @ColumnAdapter(MapServiceColumnAdapter.class) public abstract MapService mapService(); - @NonNull + @Nullable @ColumnAdapter(ImageFormatColumnAdapter.class) public abstract ImageFormat imageFormat(); - @NonNull + @Nullable public abstract String layers(); public static MapLayer create(Cursor cursor) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt index 19e76e64d8..d6ecbba57f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayer.kt @@ -32,15 +32,15 @@ import org.hisp.dhis.android.core.map.layer.ImageFormat import org.hisp.dhis.android.core.map.layer.MapLayerPosition import org.hisp.dhis.android.core.map.layer.MapService -data class ExternalMapLayer( +internal data class ExternalMapLayer( val id: String, val name: String, val displayName: String, - val code: String, + val code: String? = null, val url: String, - val attribution: String, + val attribution: String? = null, val mapService: MapService, - val imageFormat: ImageFormat, - val layers: String, + val imageFormat: ImageFormat? = null, + val layers: String? = null, val mapLayerPosition: MapLayerPosition, ) From 5a0860967e7de82b250af59569748b24ff39acb1 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 13 Feb 2024 08:55:33 +0100 Subject: [PATCH 076/222] [ANDROSDK-1800] Add services to DI --- .../android/core/arch/api/internal/ServicesDIModule.kt | 2 ++ .../internal/externalmap/ExternalMapLayerCallFactory.kt | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt index 88afbf8e25..6e7b593fd0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt @@ -19,6 +19,7 @@ import org.hisp.dhis.android.core.indicator.internal.IndicatorService import org.hisp.dhis.android.core.indicator.internal.IndicatorTypeService import org.hisp.dhis.android.core.legendset.internal.LegendSetService import org.hisp.dhis.android.core.map.layer.internal.bing.BingService +import org.hisp.dhis.android.core.map.layer.internal.externalmap.ExternalMapLayerService import org.hisp.dhis.android.core.option.internal.OptionGroupService import org.hisp.dhis.android.core.option.internal.OptionService import org.hisp.dhis.android.core.option.internal.OptionSetService @@ -69,6 +70,7 @@ internal val servicesDIModule = module { single { get().create(EventFilterService::class.java) } single { get().create(EventService::class.java) } single { get().create(ExpressionDimensionItemService::class.java) } + single { get().create(ExternalMapLayerService::class.java) } single { get().create(FileResourceService::class.java) } single { get().create(IndicatorService::class.java) } single { get().create(IndicatorTypeService::class.java) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt index 19b7267edb..2370734eba 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.map.layer.internal.externalmap import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader import org.hisp.dhis.android.core.map.layer.MapLayer import org.hisp.dhis.android.core.map.layer.MapLayerImageryProvider +import org.hisp.dhis.android.core.map.layer.MapLayerPosition import org.hisp.dhis.android.core.map.layer.internal.MapLayerHandler import org.koin.core.annotation.Singleton @@ -48,7 +49,11 @@ internal class ExternalMapLayerCallFactory( } private suspend fun getExternalMapLayers(): List { - return service.getExternalMapLayers(ExternalMapLayerFields.allFields, false).items() + return service.getExternalMapLayers( + ExternalMapLayerFields.allFields, + ExternalMapLayerFields.mapLayerPosition.eq(MapLayerPosition.BASEMAP), + false, + ).items() .map { externalMapLayer -> MapLayer.builder() .uid(externalMapLayer.id) From 797a24eb1edf1d0f4554b595ca49e8e44d631434 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 13 Feb 2024 08:55:56 +0100 Subject: [PATCH 077/222] [ANDROSDK-1800] Download only the basemaps --- .../map/layer/internal/externalmap/ExternalMapLayerFields.kt | 5 +++++ .../layer/internal/externalmap/ExternalMapLayerService.kt | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt index b858dc923c..0793629a5f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerFields.kt @@ -27,9 +27,11 @@ */ package org.hisp.dhis.android.core.map.layer.internal.externalmap +import org.hisp.dhis.android.core.arch.api.fields.internal.Field import org.hisp.dhis.android.core.arch.api.fields.internal.Fields import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper import org.hisp.dhis.android.core.map.layer.MapLayerImageryProviderTableInfo +import org.hisp.dhis.android.core.map.layer.MapLayerPosition import org.hisp.dhis.android.core.map.layer.MapLayerTableInfo internal object ExternalMapLayerFields { @@ -39,6 +41,9 @@ internal object ExternalMapLayerFields { private val fh = FieldsHelper() val uid = fh.uid() + val mapLayerPosition: Field = + Field.create(MapLayerTableInfo.Columns.MAP_LAYER_POSITION) + val allFields: Fields = Fields.builder() .fields(fh.getIdentifiableFields()) .fields( diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt index 5a065c454a..089cde3307 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt @@ -29,8 +29,11 @@ package org.hisp.dhis.android.core.map.layer.internal.externalmap import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.api.filters.internal.Filter +import org.hisp.dhis.android.core.arch.api.filters.internal.Where import org.hisp.dhis.android.core.arch.api.filters.internal.Which import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.map.layer.MapLayerPosition import retrofit2.http.GET import retrofit2.http.Query @@ -39,6 +42,7 @@ internal interface ExternalMapLayerService { @GET("externalMapLayers") suspend fun getExternalMapLayers( @Query("fields") @Which fields: Fields, + @Query("filter") @Where mapLayerPosition: Filter, @Query("paging") paging: Boolean, ): Payload } From e0698d394cf506cdf06c87e5c62968628db5f59c Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 13 Feb 2024 09:37:11 +0100 Subject: [PATCH 078/222] [ANDROSDK-1800] Make ExternalMapLayerService a functional interface --- .../map/layer/internal/externalmap/ExternalMapLayerService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt index 089cde3307..54de345fde 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerService.kt @@ -37,7 +37,7 @@ import org.hisp.dhis.android.core.map.layer.MapLayerPosition import retrofit2.http.GET import retrofit2.http.Query -internal interface ExternalMapLayerService { +internal fun interface ExternalMapLayerService { @GET("externalMapLayers") suspend fun getExternalMapLayers( From 03b9dd76d7e03c93d54e2e7b314b3ea40fd4109b Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:32:21 +0100 Subject: [PATCH 079/222] [ANDROSDK-1803] Update Bing and OSM base maps id --- .../android/core/map/layer/internal/bing/BingBasemaps.kt | 8 ++++---- .../android/core/map/layer/internal/osm/OSMBaseMaps.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/bing/BingBasemaps.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/bing/BingBasemaps.kt index 61dd599ede..d6bce4ecef 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/bing/BingBasemaps.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/bing/BingBasemaps.kt @@ -31,22 +31,22 @@ package org.hisp.dhis.android.core.map.layer.internal.bing internal object BingBasemaps { val list: List = listOf( BingBasemap( - id = "ql5jVkAL1iy", + id = "bingLight", name = "Bing Road", style = "CanvasLight", ), BingBasemap( - id = "PwJ1fQoTthh", + id = "bingDark", name = "Bing Dark", style = "CanvasDark", ), BingBasemap( - id = "kKJNmY2yYtM", + id = "bingAerial", name = "Bing Aerial", style = "Aerial", ), BingBasemap( - id = "TfK2zM71AHJ", + id = "bingHybrid", name = "Bing Aerial Labels", style = "AerialWithLabelsOnDemand", ), diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/osm/OSMBaseMaps.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/osm/OSMBaseMaps.kt index e644ead7ea..b343ebcc9e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/osm/OSMBaseMaps.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/osm/OSMBaseMaps.kt @@ -31,7 +31,7 @@ package org.hisp.dhis.android.core.map.layer.internal.osm object OSMBaseMaps { val list: List = listOf( OSMBaseMap( - id = "l7rimUxoQu4", + id = "osmLight", name = "OSM Light", imageUrl = "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}@2x.png", subdomains = listOf("a", "b", "c", "d"), @@ -40,7 +40,7 @@ object OSMBaseMaps { " © Carto", ), OSMBaseMap( - id = "k6QEWMytadd", + id = "openStreetMap", name = "OSM Detailed", imageUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", subdomains = listOf("a", "b", "c", "d"), From d27e93d745ce1e6656e3e97c1cbb49d42708a1f3 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:33:07 +0100 Subject: [PATCH 080/222] [ANDROSDK-1803] Support for Default base map key in System Settings --- .../org/hisp/dhis/android/core/settings/SystemSetting.java | 3 ++- .../java/org/hisp/dhis/android/core/settings/SystemSettings.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSetting.java b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSetting.java index e82fb9eabe..d21a73a038 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSetting.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSetting.java @@ -44,7 +44,8 @@ public abstract class SystemSetting implements CoreObject { public enum SystemSettingKey { FLAG, - STYLE + STYLE, + DEFAULT_BASE_MAP, } @Nullable diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettings.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettings.kt index 23625291a5..d33eb1fb8c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettings.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettings.kt @@ -30,5 +30,6 @@ package org.hisp.dhis.android.core.settings data class SystemSettings( val keyFlag: String?, val keyStyle: String?, + val keyDefaultBaseMap: String?, val keyBingMapsApiKey: String?, ) From ec30271a7eb8d0adfc81666f5220acb8663f539f Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:33:33 +0100 Subject: [PATCH 081/222] [ANDROSDK-1803] Download default base map setting --- .../android/core/settings/internal/SystemSettingsFields.kt | 2 ++ .../core/settings/internal/SystemSettingsSplitter.kt | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsFields.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsFields.kt index 2e313d2fdb..a5939e0cf7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsFields.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.settings.SystemSettings internal object SystemSettingsFields { private const val KEY_FLAG = "keyFlag" private const val KEY_STYLE = "keyStyle" + private const val KEY_DEFAULT_BASE_MAP = "keyDefaultBaseMap" private const val KEY_BING_MAPS_API_KEY = "keyBingMapsApiKey" private val fh = FieldsHelper() @@ -42,6 +43,7 @@ internal object SystemSettingsFields { .fields( fh.field(KEY_FLAG), fh.field(KEY_STYLE), + fh.field(KEY_DEFAULT_BASE_MAP), ).build() val bingApiKey: Fields = Fields.builder() diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsSplitter.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsSplitter.kt index 1fc6606b85..518d80d0b0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsSplitter.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SystemSettingsSplitter.kt @@ -42,7 +42,11 @@ internal class SystemSettingsSplitter { .key(SystemSetting.SystemSettingKey.STYLE) .value(settings.keyStyle) .build() + val keyDefaultBaseMap = SystemSetting.builder() + .key(SystemSetting.SystemSettingKey.DEFAULT_BASE_MAP) + .value(settings.keyDefaultBaseMap) + .build() - return listOf(flag, style) + return listOf(flag, style, keyDefaultBaseMap) } } From 39ece656515c2834a1657f783cfe9684b8b6d7c8 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:34:09 +0100 Subject: [PATCH 082/222] [ANDROSDK-1803] Migrate old base map keys on database --- core/src/main/assets/migrations/159.sql | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/core/src/main/assets/migrations/159.sql b/core/src/main/assets/migrations/159.sql index 549ae49fad..c671021e79 100644 --- a/core/src/main/assets/migrations/159.sql +++ b/core/src/main/assets/migrations/159.sql @@ -1,6 +1,24 @@ -# Add external map layers (ANDROSDK-1800) +# Add external map layers (ANDROSDK-1800) and migrate map layer keys (ANDROSDK-1803) ALTER TABLE MapLayer ADD COLUMN code TEXT; ALTER TABLE MapLayer ADD COLUMN mapService TEXT; ALTER TABLE MapLayer ADD COLUMN imageFormat TEXT; -ALTER TABLE MapLayer ADD COLUMN layers TEXT; \ No newline at end of file +ALTER TABLE MapLayer ADD COLUMN layers TEXT; + +UPDATE MapLayer SET uid = 'osmLight' WHERE uid = 'l7rimUxoQu4'; +UPDATE MapLayerImageryProvider SET mapLayer = 'osmLight' WHERE mapLayer = 'l7rimUxoQu4'; + +UPDATE MapLayer SET uid = 'openStreetMap' WHERE uid = 'k6QEWMytadd'; +UPDATE MapLayerImageryProvider SET mapLayer = 'openStreetMap' WHERE mapLayer = 'k6QEWMytadd'; + +UPDATE MapLayer SET uid = 'bingLight' WHERE uid = 'ql5jVkAL1iy'; +UPDATE MapLayerImageryProvider SET mapLayer = 'bingLight' WHERE mapLayer = 'ql5jVkAL1iy'; + +UPDATE MapLayer SET uid = 'bingDark' WHERE uid = 'PwJ1fQoTthh'; +UPDATE MapLayerImageryProvider SET mapLayer = 'bingDark' WHERE mapLayer = 'PwJ1fQoTthh'; + +UPDATE MapLayer SET uid = 'bingAerial' WHERE uid = 'kKJNmY2yYtM'; +UPDATE MapLayerImageryProvider SET mapLayer = 'bingAerial' WHERE mapLayer = 'kKJNmY2yYtM'; + +UPDATE MapLayer SET uid = 'bingHybrid' WHERE uid = 'TfK2zM71AHJ'; +UPDATE MapLayerImageryProvider SET mapLayer = 'bingHybrid' WHERE mapLayer = 'TfK2zM71AHJ'; \ No newline at end of file From 5eab1345c59e4ad2d558d5d7940e9c90abb7ce63 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:34:36 +0100 Subject: [PATCH 083/222] [ANDROSDK-1803] Update SystemSettingCollectionRepository to provide default base map key --- .../core/settings/SystemSettingCollectionRepository.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettingCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettingCollectionRepository.kt index c6cd919f98..1907d0a216 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettingCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/SystemSettingCollectionRepository.kt @@ -75,6 +75,10 @@ class SystemSettingCollectionRepository internal constructor( return byKey().eq(SystemSettingKey.STYLE).one() } + fun defaultBaseMap(): ReadOnlyOneObjectRepositoryFinalImpl { + return byKey().eq(SystemSettingKey.DEFAULT_BASE_MAP).one() + } + internal companion object { val childrenAppenders: ChildrenAppenderGetter = emptyMap() } From ce0d42be9556ba577f432a66d590011dc58b9a41 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 14 Feb 2024 12:34:55 +0100 Subject: [PATCH 084/222] [ANDROSDK-1803] Add default base map key to tests --- .../SettingsModuleMockIntegrationShould.java | 18 +++++++++++++++++- .../resources/settings/system_settings.json | 1 + .../core/settings/SystemSettingsShould.kt | 1 + .../internal/SystemSettingSplitterShould.kt | 10 ++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/settings/SettingsModuleMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/settings/SettingsModuleMockIntegrationShould.java index 66ea9e05e7..637746339d 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/settings/SettingsModuleMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/settings/SettingsModuleMockIntegrationShould.java @@ -43,7 +43,7 @@ public class SettingsModuleMockIntegrationShould extends BaseMockIntegrationTest @Test public void allow_access_to_system_setting() { List systemSettings = d2.settingModule().systemSetting().blockingGet(); - assertThat(systemSettings.size()).isEqualTo(2); + assertThat(systemSettings.size()).isEqualTo(3); } @Test @@ -56,6 +56,10 @@ public void allow_access_to_system_setting_filtered_by_key() { List systemSettingsStyle = d2.settingModule().systemSetting().byKey() .eq(SystemSetting.SystemSettingKey.STYLE).blockingGet(); assertThat(systemSettingsStyle.get(0).value()).isEqualTo("light_blue/light_blue.css"); + + List systemSettingsDefaultBaseMap = d2.settingModule().systemSetting().byKey() + .eq(SystemSetting.SystemSettingKey.DEFAULT_BASE_MAP).blockingGet(); + assertThat(systemSettingsDefaultBaseMap.get(0).value()).isEqualTo("keyDefaultBaseMap"); } @Test @@ -68,6 +72,11 @@ public void allow_access_to_system_setting_filtered_by_value() { List systemSettingsStyle = d2.settingModule().systemSetting().byValue() .eq("light_blue/light_blue.css").blockingGet(); assertThat(systemSettingsStyle.get(0).key()).isEqualTo(SystemSetting.SystemSettingKey.STYLE); + + List systemSettingsDefaultBaseMap = d2.settingModule().systemSetting().byValue() + .eq("keyDefaultBaseMap").blockingGet(); + assertThat(systemSettingsDefaultBaseMap.get(0).key()) + .isEqualTo(SystemSetting.SystemSettingKey.DEFAULT_BASE_MAP); } @Test @@ -83,4 +92,11 @@ public void allow_access_to_style_settings() { assertThat(systemSetting.key()).isEqualTo(SystemSetting.SystemSettingKey.STYLE); assertThat(systemSetting.value()).isEqualTo("light_blue/light_blue.css"); } + + @Test + public void allow_access_to_default_base_map_settings() { + SystemSetting systemSetting = d2.settingModule().systemSetting().defaultBaseMap().blockingGet(); + assertThat(systemSetting.key()).isEqualTo(SystemSetting.SystemSettingKey.DEFAULT_BASE_MAP); + assertThat(systemSetting.value()).isEqualTo("keyDefaultBaseMap"); + } } \ No newline at end of file diff --git a/core/src/sharedTest/resources/settings/system_settings.json b/core/src/sharedTest/resources/settings/system_settings.json index 162df22f97..a639a34555 100644 --- a/core/src/sharedTest/resources/settings/system_settings.json +++ b/core/src/sharedTest/resources/settings/system_settings.json @@ -1,5 +1,6 @@ { "keyFlag": "sierra_leone", "keyStyle": "light_blue/light_blue.css", + "keyDefaultBaseMap": "keyDefaultBaseMap", "keyBingMapsApiKey": "keyBingMapsApiKey" } \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/SystemSettingsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/SystemSettingsShould.kt index ac303231b5..184dc15c2c 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/SystemSettingsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/SystemSettingsShould.kt @@ -38,6 +38,7 @@ class SystemSettingsShould : BaseObjectShould("settings/system_settings.json"), val settings = objectMapper.readValue(jsonStream, SystemSettings::class.java) assertThat(settings.keyFlag).isEqualTo("sierra_leone") assertThat(settings.keyStyle).isEqualTo("light_blue/light_blue.css") + assertThat(settings.keyDefaultBaseMap).isEqualTo("keyDefaultBaseMap") assertThat(settings.keyBingMapsApiKey).isEqualTo("keyBingMapsApiKey") } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/SystemSettingSplitterShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/SystemSettingSplitterShould.kt index 2601fef651..a9c3740075 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/SystemSettingSplitterShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/SystemSettingSplitterShould.kt @@ -39,6 +39,7 @@ class SystemSettingSplitterShould { private val settings: SystemSettings = SystemSettings( keyFlag = "aFlag", keyStyle = "aStyle", + keyDefaultBaseMap = "aDefaultBaseMap", keyBingMapsApiKey = null, ) @@ -61,4 +62,13 @@ class SystemSettingSplitterShould { assertThat(style.value()).isEqualTo("aStyle") } } + + @Test + fun build_default_base_map_setting() { + val settingList = systemSettingsSplitter.splitSettings(settings) + settingList[2].let { style -> + assertThat(style.key()).isEqualTo(SystemSettingKey.DEFAULT_BASE_MAP) + assertThat(style.value()).isEqualTo("aDefaultBaseMap") + } + } } From b88b7de746f5e1024b3c49ba5e7a0a858e103a2f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 9 Feb 2024 13:03:45 +0100 Subject: [PATCH 085/222] [ANDROSDK-1630] Add CustomIcon model --- .../MetadataCallMockIntegrationShould.kt | 4 +- .../CustomIconStoreIntegrationShould.kt | 54 + core/src/main/assets/migrations/160.sql | 3 + core/src/main/assets/snapshots/snapshot.sql | 1 + .../arch/api/internal/ServicesDIModule.kt | 2 + .../arch/d2/internal/D2DIComponentFactory.kt | 2 + .../access/internal/BaseDatabaseOpenHelper.kt | 2 +- .../db/uidseeker/internal/BaseUidsSeeker.kt | 2 +- .../core/arch/fields/internal/FieldsHelper.kt | 2 +- .../internal/ObjectWithoutUidHandlerImpl.kt | 2 +- .../internal/TableWithObjectStyle.kt | 58 ++ .../core/domain/metadata/MetadataCall.kt | 10 +- .../dhis/android/core/icon/CustomIcon.java | 83 ++ .../android/core/icon/CustomIconTableInfo.kt | 71 ++ .../dhis/android/core/icon/DefaultIcon.kt | 931 ++++++++++++++++++ .../dhis/android/core/icon/IconDIModule.kt | 35 + .../core/icon/internal/CustomIconCall.kt | 72 ++ .../core/icon/internal/CustomIconFields.kt | 51 + .../core/icon/internal/CustomIconHandler.kt | 38 + .../internal/CustomIconModuleDownloader.kt | 49 + .../core/icon/internal/CustomIconSeeker.kt | 50 + .../core/icon/internal/CustomIconStore.kt | 34 + .../core/icon/internal/CustomIconStoreImpl.kt | 68 ++ .../core/icon/internal/IconModuleWiper.kt | 46 + .../android/core/icon/internal/IconService.kt | 49 + .../core/wipe/internal/D2ModuleWipers.kt | 3 + .../core/data/icon/CustomIconSamples.kt | 43 + .../resources/icon/custom_icon.json | 5 + .../android/core/icon/CustomIconShould.kt | 47 + 29 files changed, 1810 insertions(+), 7 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt create mode 100644 core/src/main/assets/migrations/160.sql create mode 100644 core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/IconDIModule.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconHandler.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt create mode 100644 core/src/sharedTest/resources/icon/custom_icon.json create mode 100644 core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt index b05fe2076b..8faf33711f 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.category.Category import org.hisp.dhis.android.core.constant.Constant import org.hisp.dhis.android.core.dataset.DataSet import org.hisp.dhis.android.core.expressiondimensionitem.ExpressionDimensionItem +import org.hisp.dhis.android.core.icon.CustomIcon import org.hisp.dhis.android.core.indicator.Indicator import org.hisp.dhis.android.core.legendset.LegendSet import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @@ -65,7 +66,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher testObserver.awaitTerminalEvent() - testObserver.assertValueCount(17) + testObserver.assertValueCount(18) val values = testObserver.values() @@ -94,6 +95,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher LegendSet::class, Attribute::class, ExpressionDimensionItem::class, + CustomIcon::class ).map { it.java.simpleName }, ) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt new file mode 100644 index 0000000000..c12bf0625e --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.data.database.ObjectWithoutUidStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.icon.CustomIconSamples +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class CustomIconStoreIntegrationShould : + ObjectWithoutUidStoreAbstractIntegrationShould( + CustomIconStoreImpl(TestDatabaseAdapterFactory.get()), + CustomIconTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), + ) { + override fun buildObject(): CustomIcon { + return CustomIconSamples.getCustomIcon() + } + + override fun buildObjectToUpdate(): CustomIcon { + return CustomIconSamples.getCustomIcon().toBuilder() + .fileResourceUid("otherResourceUid") + .build() + } +} diff --git a/core/src/main/assets/migrations/160.sql b/core/src/main/assets/migrations/160.sql new file mode 100644 index 0000000000..146e7cd249 --- /dev/null +++ b/core/src/main/assets/migrations/160.sql @@ -0,0 +1,3 @@ +# Add CustomIcon model (ANDROSDK-1630) + +CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index bf725ddfd3..4d0421c938 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -132,3 +132,4 @@ CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadUR CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt index 6e7b593fd0..c7de8738ef 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt @@ -15,6 +15,7 @@ import org.hisp.dhis.android.core.event.internal.EventFilterService import org.hisp.dhis.android.core.event.internal.EventService import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemService import org.hisp.dhis.android.core.fileresource.internal.FileResourceService +import org.hisp.dhis.android.core.icon.internal.IconService import org.hisp.dhis.android.core.indicator.internal.IndicatorService import org.hisp.dhis.android.core.indicator.internal.IndicatorTypeService import org.hisp.dhis.android.core.legendset.internal.LegendSetService @@ -72,6 +73,7 @@ internal val servicesDIModule = module { single { get().create(ExpressionDimensionItemService::class.java) } single { get().create(ExternalMapLayerService::class.java) } single { get().create(FileResourceService::class.java) } + single { get().create(IconService::class.java)} single { get().create(IndicatorService::class.java) } single { get().create(IndicatorTypeService::class.java) } single { get().create(LegendSetService::class.java) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponentFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponentFactory.kt index e717822123..7d810bde6e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponentFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponentFactory.kt @@ -50,6 +50,7 @@ import org.hisp.dhis.android.core.enrollment.EnrollmentDIModule import org.hisp.dhis.android.core.event.EventDIModule import org.hisp.dhis.android.core.expressiondimensionitem.ExpressionDimensionItemDIModule import org.hisp.dhis.android.core.fileresource.FileResourceDIModule +import org.hisp.dhis.android.core.icon.IconDIModule import org.hisp.dhis.android.core.imports.ImportsDIModule import org.hisp.dhis.android.core.indicator.IndicatorDIModule import org.hisp.dhis.android.core.legendset.LegendSetDIModule @@ -109,6 +110,7 @@ internal object D2DIComponentFactory { EventDIModule().module, ExpressionDimensionItemDIModule().module, FileResourceDIModule().module, + IconDIModule().module, ImportsDIModule().module, IndicatorDIModule().module, LegendSetDIModule().module, diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index ddda39d45e..31ca52da58 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 159 + const val VERSION = 160 } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/uidseeker/internal/BaseUidsSeeker.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/uidseeker/internal/BaseUidsSeeker.kt index da243f6167..ce5803d542 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/uidseeker/internal/BaseUidsSeeker.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/uidseeker/internal/BaseUidsSeeker.kt @@ -30,7 +30,7 @@ package org.hisp.dhis.android.core.arch.db.uidseeker.internal import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter -internal open class BaseUidsSeeker constructor(private val databaseAdapter: DatabaseAdapter) { +internal open class BaseUidsSeeker(private val databaseAdapter: DatabaseAdapter) { fun readSingleColumnResults(query: String): Set { val cursor = databaseAdapter.rawQuery(query) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/fields/internal/FieldsHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/fields/internal/FieldsHelper.kt index b434ac5ebe..7e0500996b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/fields/internal/FieldsHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/fields/internal/FieldsHelper.kt @@ -36,7 +36,7 @@ import org.hisp.dhis.android.core.common.ObjectWithUid @Suppress("TooManyFunctions") internal class FieldsHelper { - fun field(fieldName: String): Property { + fun field(fieldName: String): Field { return Field.create(fieldName) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/ObjectWithoutUidHandlerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/ObjectWithoutUidHandlerImpl.kt index cbdef7bca5..afd2048a1e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/ObjectWithoutUidHandlerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/handlers/internal/ObjectWithoutUidHandlerImpl.kt @@ -30,7 +30,7 @@ package org.hisp.dhis.android.core.arch.handlers.internal import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectWithoutUidStore import org.hisp.dhis.android.core.common.CoreObject -internal open class ObjectWithoutUidHandlerImpl(protected val store: ObjectWithoutUidStore) : +internal open class ObjectWithoutUidHandlerImpl(protected val store: ObjectWithoutUidStore) : HandlerBaseImpl() { override fun deleteOrPersist(o: O): HandleAction { diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt new file mode 100644 index 0000000000..5f303c5391 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.common.objectstyle.internal + +import org.hisp.dhis.android.core.dataelement.DataElementTableInfo +import org.hisp.dhis.android.core.dataset.DataSetTableInfo +import org.hisp.dhis.android.core.indicator.IndicatorTableInfo +import org.hisp.dhis.android.core.option.OptionTableInfo +import org.hisp.dhis.android.core.program.ProgramSectionTableInfo +import org.hisp.dhis.android.core.program.ProgramStageTableInfo +import org.hisp.dhis.android.core.program.ProgramTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceFilterTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeTableInfo + +internal object TableWithObjectStyle { + //TODO Test table containing icon + val allTableNames: List = setOf( + DataElementTableInfo.TABLE_INFO, + DataSetTableInfo.TABLE_INFO, + IndicatorTableInfo.TABLE_INFO, + OptionTableInfo.TABLE_INFO, + ProgramSectionTableInfo.TABLE_INFO, + ProgramStageTableInfo.TABLE_INFO, + ProgramTableInfo.TABLE_INFO, + TrackedEntityAttributeTableInfo.TABLE_INFO, + TrackedEntityInstanceFilterTableInfo.TABLE_INFO, + TrackedEntityTypeTableInfo.TABLE_INFO + ).map { + it.name() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt index a24e4cb19e..a8f5a05c93 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/metadata/MetadataCall.kt @@ -48,6 +48,8 @@ import org.hisp.dhis.android.core.dataset.DataSet import org.hisp.dhis.android.core.dataset.internal.DataSetModuleDownloader import org.hisp.dhis.android.core.expressiondimensionitem.ExpressionDimensionItem import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemModuleDownloader +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.internal.CustomIconModuleDownloader import org.hisp.dhis.android.core.indicator.Indicator import org.hisp.dhis.android.core.indicator.internal.IndicatorModuleDownloader import org.hisp.dhis.android.core.legendset.LegendSet @@ -100,10 +102,11 @@ internal class MetadataCall( private val legendSetModuleDownloader: LegendSetModuleDownloader, private val attributeModuleDownloader: AttributeModuleDownloader, private val expressionDimensionItemModuleDownloader: ExpressionDimensionItemModuleDownloader, + private val customIconDownloader: CustomIconModuleDownloader, ) { companion object { - const val CALLS_COUNT = 16 + const val CALLS_COUNT = 17 } @Suppress("TooGenericExceptionCaught") @@ -178,7 +181,10 @@ internal class MetadataCall( emit(progressManager.increaseProgress(Attribute::class.java, false)) expressionDimensionItemModuleDownloader.downloadMetadata() - emit(progressManager.increaseProgress(ExpressionDimensionItem::class.java, true)) + emit(progressManager.increaseProgress(ExpressionDimensionItem::class.java, false)) + + customIconDownloader.downloadMetadata() + emit(progressManager.increaseProgress(CustomIcon::class.java, true)) } private suspend fun changeEncryptionIfRequiredCoroutines() { diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java new file mode 100644 index 0000000000..ed695791e2 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.common.CoreObject; + +@AutoValue +@JsonDeserialize(builder = AutoValue_CustomIcon.Builder.class) +public abstract class CustomIcon implements CoreObject { + + @NonNull + @JsonProperty() + public abstract String key(); + + @NonNull + @JsonProperty + public abstract String fileResourceUid(); + + @NonNull + @JsonProperty + public abstract String href(); + + @NonNull + public static CustomIcon create(Cursor cursor) { + return AutoValue_CustomIcon.createFromCursor(cursor); + } + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_CustomIcon.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder { + + public abstract Builder id(Long id); + + public abstract Builder key(String key); + + public abstract Builder fileResourceUid(String fileResourceUid); + + public abstract Builder href(String href); + + public abstract CustomIcon build(); + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt new file mode 100644 index 0000000000..9365fff257 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns + +object CustomIconTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "CustomIcon" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : CoreColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + KEY, + FILE_RESOURCE_UID, + HREF, + ) + } + + override fun whereUpdate(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + KEY, + ) + } + + companion object { + const val KEY = "key" + const val FILE_RESOURCE_UID = "fileResourceUid" + const val HREF = "href" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt new file mode 100644 index 0000000000..707bbc1eb5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt @@ -0,0 +1,931 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon + +internal object DefaultIcon { + val all = listOf( + "2g_negative", + "2g_outline", + "2g_positive", + "3g_negative", + "3g_outline", + "3g_positive", + "4x4_negative", + "4x4_outline", + "4x4_positive", + "agriculture_negative", + "agriculture_outline", + "agriculture_positive", + "agriculture_worker_negative", + "agriculture_worker_outline", + "agriculture_worker_positive", + "alert_circle_negative", + "alert_circle_outline", + "alert_circle_positive", + "alert_negative", + "alert_outline", + "alert_positive", + "alert_triangle_negative", + "alert_triangle_outline", + "alert_triangle_positive", + "ambulance_negative", + "ambulance_outline", + "ambulance_positive", + "ambulatory_clinic_negative", + "ambulatory_clinic_outline", + "ambulatory_clinic_positive", + "ancv_negative", + "ancv_outline", + "ancv_positive", + "baby_female_0203m_negative", + "baby_female_0203m_outline", + "baby_female_0203m_positive", + "baby_female_0306m_negative", + "baby_female_0306m_outline", + "baby_female_0306m_positive", + "baby_female_0609m_negative", + "baby_female_0609m_outline", + "baby_female_0609m_positive", + "baby_male_0203m_negative", + "baby_male_0203m_outline", + "baby_male_0203m_positive", + "baby_male_0306m_negative", + "baby_male_0306m_outline", + "baby_male_0306m_positive", + "baby_male_0609m_negative", + "baby_male_0609m_outline", + "baby_male_0609m_positive", + "basic_motorcycle_negative", + "basic_motorcycle_outline", + "basic_motorcycle_positive", + "bike_negative", + "bike_outline", + "bike_positive", + "bills_negative", + "bills_outline", + "bills_positive", + "blister_pills_oval_x14_negative", + "blister_pills_oval_x14_outline", + "blister_pills_oval_x14_positive", + "blister_pills_oval_x16_negative", + "blister_pills_oval_x16_outline", + "blister_pills_oval_x16_positive", + "blister_pills_oval_x1_negative", + "blister_pills_oval_x1_outline", + "blister_pills_oval_x1_positive", + "blister_pills_oval_x4_negative", + "blister_pills_oval_x4_outline", + "blister_pills_oval_x4_positive", + "blister_pills_round_x14_negative", + "blister_pills_round_x14_outline", + "blister_pills_round_x14_positive", + "blister_pills_round_x16_negative", + "blister_pills_round_x16_outline", + "blister_pills_round_x16_positive", + "blister_pills_round_x1_negative", + "blister_pills_round_x1_outline", + "blister_pills_round_x1_positive", + "blister_pills_round_x4_negative", + "blister_pills_round_x4_outline", + "blister_pills_round_x4_positive", + "blood_a_n_negative", + "blood_a_n_outline", + "blood_a_n_positive", + "blood_a_p_negative", + "blood_a_p_outline", + "blood_a_p_positive", + "blood_ab_n_negative", + "blood_ab_n_outline", + "blood_ab_n_positive", + "blood_ab_p_negative", + "blood_ab_p_outline", + "blood_ab_p_positive", + "blood_b_n_negative", + "blood_b_n_outline", + "blood_b_n_positive", + "blood_b_p_negative", + "blood_b_p_outline", + "blood_b_p_positive", + "blood_o_n_negative", + "blood_o_n_outline", + "blood_o_n_positive", + "blood_o_p_negative", + "blood_o_p_outline", + "blood_o_p_positive", + "blood_pressure_2_negative", + "blood_pressure_2_outline", + "blood_pressure_2_positive", + "blood_pressure_monitor_negative", + "blood_pressure_monitor_outline", + "blood_pressure_monitor_positive", + "blood_pressure_negative", + "blood_pressure_outline", + "blood_pressure_positive", + "blood_rh_n_negative", + "blood_rh_n_outline", + "blood_rh_n_positive", + "blood_rh_p_negative", + "blood_rh_p_outline", + "blood_rh_p_positive", + "boy_0105y_negative", + "boy_0105y_outline", + "boy_0105y_positive", + "boy_1015y_negative", + "boy_1015y_outline", + "boy_1015y_positive", + "breeding_sites_negative", + "breeding_sites_outline", + "breeding_sites_positive", + "calendar_negative", + "calendar_outline", + "calendar_positive", + "cardiogram_e_negative", + "cardiogram_e_outline", + "cardiogram_e_positive", + "cardiogram_negative", + "cardiogram_outline", + "cardiogram_positive", + "cervical_cancer_negative", + "cervical_cancer_outline", + "cervical_cancer_positive", + "child_care_negative", + "child_care_outline", + "child_care_positive", + "child_program_negative", + "child_program_outline", + "child_program_positive", + "chills_negative", + "chills_outline", + "chills_positive", + "cholera_negative", + "cholera_outline", + "cholera_positive", + "church_negative", + "church_outline", + "church_positive", + "circle_large_negative", + "circle_large_outline", + "circle_large_positive", + "circle_medium_negative", + "circle_medium_outline", + "circle_medium_positive", + "circle_small_negative", + "circle_small_outline", + "circle_small_positive", + "city_negative", + "city_outline", + "city_positive", + "city_worker_negative", + "city_worker_outline", + "city_worker_positive", + "clean_hands_negative", + "clean_hands_outline", + "clean_hands_positive", + "clinical_a_negative", + "clinical_a_outline", + "clinical_a_positive", + "clinical_f_negative", + "clinical_f_outline", + "clinical_f_positive", + "clinical_fe_negative", + "clinical_fe_outline", + "clinical_fe_positive", + "coins_negative", + "coins_outline", + "coins_positive", + "cold_chain_negative", + "cold_chain_outline", + "cold_chain_positive", + "communication_negative", + "communication_outline", + "communication_positive", + "cone_test_on_nets_negative", + "cone_test_on_nets_outline", + "cone_test_on_nets_positive", + "cone_test_on_walls_negative", + "cone_test_on_walls_outline", + "cone_test_on_walls_positive", + "construction_negative", + "construction_outline", + "construction_positive", + "construction_worker_negative", + "construction_worker_outline", + "construction_worker_positive", + "contact_support_negative", + "contact_support_outline", + "contact_support_positive", + "contraceptive_diaphragm_negative", + "contraceptive_diaphragm_outline", + "contraceptive_diaphragm_positive", + "contraceptive_injection_negative", + "contraceptive_injection_outline", + "contraceptive_injection_positive", + "contraceptive_patch_negative", + "contraceptive_patch_outline", + "contraceptive_patch_positive", + "contraceptive_voucher_negative", + "contraceptive_voucher_outline", + "contraceptive_voucher_positive", + "copper_iud_negative", + "copper_iud_outline", + "copper_iud_positive", + "coughing_negative", + "coughing_outline", + "coughing_positive", + "credit_card_negative", + "credit_card_outline", + "credit_card_positive", + "cross_country_motorcycle_negative", + "cross_country_motorcycle_outline", + "cross_country_motorcycle_positive", + "default_negative", + "default_outline", + "default_positive", + "dhis2_logo_negative", + "dhis2_logo_outline", + "dhis2_logo_positive", + "diarrhea_negative", + "diarrhea_outline", + "diarrhea_positive", + "discriminating_concentration_bioassays_negative", + "discriminating_concentration_bioassays_outline", + "discriminating_concentration_bioassays_positive", + "doctor_negative", + "doctor_outline", + "doctor_positive", + "domestic_worker_negative", + "domestic_worker_outline", + "domestic_worker_positive", + "donkey_negative", + "donkey_outline", + "donkey_positive", + "drone_negative", + "drone_outline", + "drone_positive", + "eco_care_negative", + "eco_care_outline", + "eco_care_positive", + "elderly_negative", + "elderly_outline", + "elderly_positive", + "electricity_negative", + "electricity_outline", + "electricity_positive", + "emergency_post_negative", + "emergency_post_outline", + "emergency_post_positive", + "expectorate_negative", + "expectorate_outline", + "expectorate_positive", + "factory_worker_negative", + "factory_worker_outline", + "factory_worker_positive", + "family_planning_negative", + "family_planning_outline", + "family_planning_positive", + "female_and_male_negative", + "female_and_male_outline", + "female_and_male_positive", + "female_condom_negative", + "female_condom_outline", + "female_condom_positive", + "female_sex_worker_negative", + "female_sex_worker_outline", + "female_sex_worker_positive", + "fetus_negative", + "fetus_outline", + "fetus_positive", + "fever_2_negative", + "fever_2_outline", + "fever_2_positive", + "fever_chills_negative", + "fever_chills_outline", + "fever_chills_positive", + "fever_negative", + "fever_outline", + "fever_positive", + "forest_negative", + "forest_outline", + "forest_persons_negative", + "forest_persons_outline", + "forest_persons_positive", + "forest_positive", + "forum_negative", + "forum_outline", + "forum_positive", + "girl_0105y_negative", + "girl_0105y_outline", + "girl_0105y_positive", + "girl_1015y_negative", + "girl_1015y_outline", + "girl_1015y_positive", + "group_discussion_meeting_negative", + "group_discussion_meeting_outline", + "group_discussion_meeting_positive", + "group_discussion_meetingx3_negative", + "group_discussion_meetingx3_outline", + "group_discussion_meetingx3_positive", + "happy_negative", + "happy_outline", + "happy_positive", + "hazardous_negative", + "hazardous_outline", + "hazardous_positive", + "headache_negative", + "headache_outline", + "headache_positive", + "health_worker_form_negative", + "health_worker_form_outline", + "health_worker_form_positive", + "health_worker_negative", + "health_worker_outline", + "health_worker_positive", + "heart_cardiogram_negative", + "heart_cardiogram_outline", + "heart_cardiogram_positive", + "heart_negative", + "heart_outline", + "heart_positive", + "helicopter_negative", + "helicopter_outline", + "helicopter_positive", + "high_bars_negative", + "high_bars_outline", + "high_bars_positive", + "high_level_negative", + "high_level_outline", + "high_level_positive", + "hiv_ind_negative", + "hiv_ind_outline", + "hiv_ind_positive", + "hiv_neg_negative", + "hiv_neg_outline", + "hiv_neg_positive", + "hiv_pos_negative", + "hiv_pos_outline", + "hiv_pos_positive", + "hiv_self_test_negative", + "hiv_self_test_outline", + "hiv_self_test_positive", + "home_negative", + "home_outline", + "home_positive", + "hormonal_ring_negative", + "hormonal_ring_outline", + "hormonal_ring_positive", + "hospital_negative", + "hospital_outline", + "hospital_positive", + "hospitalized_negative", + "hospitalized_outline", + "hospitalized_positive", + "hot_meal_negative", + "hot_meal_outline", + "hot_meal_positive", + "hpv_negative", + "hpv_outline", + "hpv_positive", + "i_certificate_paper_negative", + "i_certificate_paper_outline", + "i_certificate_paper_positive", + "i_documents_accepted_negative", + "i_documents_accepted_outline", + "i_documents_accepted_positive", + "i_documents_denied_negative", + "i_documents_denied_outline", + "i_documents_denied_positive", + "i_exam_multiple_choice_negative", + "i_exam_multiple_choice_outline", + "i_exam_multiple_choice_positive", + "i_exam_qualification_negative", + "i_exam_qualification_outline", + "i_exam_qualification_positive", + "i_groups_perspective_crowd_negative", + "i_groups_perspective_crowd_outline", + "i_groups_perspective_crowd_positive", + "i_note_action_negative", + "i_note_action_outline", + "i_note_action_positive", + "i_schedule_school_date_time_negative", + "i_schedule_school_date_time_outline", + "i_schedule_school_date_time_positive", + "i_training_class_negative", + "i_training_class_outline", + "i_training_class_positive", + "i_utensils_negative", + "i_utensils_outline", + "i_utensils_positive", + "imm_negative", + "imm_outline", + "imm_positive", + "implant_negative", + "implant_outline", + "implant_positive", + "info_negative", + "info_outline", + "info_positive", + "information_campaign_negative", + "information_campaign_outline", + "information_campaign_positive", + "inpatient_negative", + "inpatient_outline", + "inpatient_positive", + "insecticide_resistance_negative", + "insecticide_resistance_outline", + "insecticide_resistance_positive", + "intensity_concentration_bioassays_negative", + "intensity_concentration_bioassays_outline", + "intensity_concentration_bioassays_positive", + "iud_negative", + "iud_outline", + "iud_positive", + "justice_negative", + "justice_outline", + "justice_positive", + "lactation_negative", + "lactation_outline", + "lactation_positive", + "letrina_negative", + "letrina_outline", + "letrina_positive", + "llin_negative", + "llin_outline", + "llin_positive", + "low_bars_negative", + "low_bars_outline", + "low_bars_positive", + "low_level_negative", + "low_level_outline", + "low_level_positive", + "machinery_negative", + "machinery_outline", + "machinery_positive", + "magnifying_glass_negative", + "magnifying_glass_outline", + "magnifying_glass_positive", + "malaria_mixed_microscope_negative", + "malaria_mixed_microscope_outline", + "malaria_mixed_microscope_positive", + "malaria_negative_microscope_negative", + "malaria_negative_microscope_outline", + "malaria_negative_microscope_positive", + "malaria_outbreak_negative", + "malaria_outbreak_outline", + "malaria_outbreak_positive", + "malaria_pf_microscope_negative", + "malaria_pf_microscope_outline", + "malaria_pf_microscope_positive", + "malaria_pv_microscope_negative", + "malaria_pv_microscope_outline", + "malaria_pv_microscope_positive", + "malaria_testing_negative", + "malaria_testing_outline", + "malaria_testing_positive", + "male_and_female_negative", + "male_and_female_outline", + "male_and_female_positive", + "male_condom_negative", + "male_condom_outline", + "male_condom_positive", + "male_sex_worker_negative", + "male_sex_worker_outline", + "male_sex_worker_positive", + "man_negative", + "man_outline", + "man_positive", + "market_stall_negative", + "market_stall_outline", + "market_stall_positive", + "mask_negative", + "mask_outline", + "mask_positive", + "measles_negative", + "measles_outline", + "measles_positive", + "medicines_negative", + "medicines_outline", + "medicines_positive", + "medium_bars_negative", + "medium_bars_outline", + "medium_bars_positive", + "medium_level_negative", + "medium_level_outline", + "medium_level_positive", + "megaphone_negative", + "megaphone_outline", + "megaphone_positive", + "mental_disorders_negative", + "mental_disorders_outline", + "mental_disorders_positive", + "microscope_negative", + "microscope_outline", + "microscope_positive", + "military_worker_negative", + "military_worker_outline", + "military_worker_positive", + "miner_worker_negative", + "miner_worker_outline", + "miner_worker_positive", + "mobile_clinic_negative", + "mobile_clinic_outline", + "mobile_clinic_positive", + "money_bag_negative", + "money_bag_outline", + "money_bag_positive", + "mosquito_collection_negative", + "mosquito_collection_outline", + "mosquito_collection_positive", + "mosquito_negative", + "mosquito_outline", + "mosquito_positive", + "msm_negative", + "msm_outline", + "msm_positive", + "nausea_negative", + "nausea_outline", + "nausea_positive", + "negative_negative", + "negative_outline", + "negative_positive", + "network_4g_negative", + "network_4g_outline", + "network_4g_positive", + "network_5g_negative", + "network_5g_outline", + "network_5g_positive", + "neurology_negative", + "neurology_outline", + "neurology_positive", + "neutral_negative", + "neutral_outline", + "neutral_positive", + "no_negative", + "no_outline", + "no_positive", + "not_ok_negative", + "not_ok_outline", + "not_ok_positive", + "nurse_negative", + "nurse_outline", + "nurse_positive", + "observation_negative", + "observation_outline", + "observation_positive", + "odontology_implant_negative", + "odontology_implant_outline", + "odontology_implant_positive", + "odontology_negative", + "odontology_outline", + "odontology_positive", + "officer_negative", + "officer_outline", + "officer_positive", + "ok_negative", + "ok_outline", + "ok_positive", + "old_man_negative", + "old_man_outline", + "old_man_positive", + "old_woman_negative", + "old_woman_outline", + "old_woman_positive", + "oral_contraception_pillsx21_negative", + "oral_contraception_pillsx21_outline", + "oral_contraception_pillsx21_positive", + "oral_contraception_pillsx28_negative", + "oral_contraception_pillsx28_outline", + "oral_contraception_pillsx28_positive", + "outpatient_negative", + "outpatient_outline", + "outpatient_positive", + "overweight_negative", + "overweight_outline", + "overweight_positive", + "palm_branches_roof_negative", + "palm_branches_roof_outline", + "palm_branches_roof_positive", + "pave_road_negative", + "pave_road_outline", + "pave_road_positive", + "peace_negative", + "peace_outline", + "peace_positive", + "people_negative", + "people_outline", + "people_positive", + "person_negative", + "person_outline", + "person_positive", + "phone_negative", + "phone_outline", + "phone_positive", + "pill_1_negative", + "pill_1_outline", + "pill_1_positive", + "pills_2_negative", + "pills_2_outline", + "pills_2_positive", + "pills_3_negative", + "pills_3_outline", + "pills_3_positive", + "pills_4_negative", + "pills_4_outline", + "pills_4_positive", + "plantation_worker_negative", + "plantation_worker_outline", + "plantation_worker_positive", + "polygon_negative", + "polygon_outline", + "polygon_positive", + "positive_negative", + "positive_outline", + "positive_positive", + "pregnant_0812w_negative", + "pregnant_0812w_outline", + "pregnant_0812w_positive", + "pregnant_2426w_negative", + "pregnant_2426w_outline", + "pregnant_2426w_positive", + "pregnant_32w_negative", + "pregnant_32w_outline", + "pregnant_32w_positive", + "pregnant_3638w_negative", + "pregnant_3638w_outline", + "pregnant_3638w_positive", + "pregnant_negative", + "pregnant_outline", + "pregnant_positive", + "prisoner_negative", + "prisoner_outline", + "prisoner_positive", + "proper_roof_negative", + "proper_roof_outline", + "proper_roof_positive", + "provider_fst_negative", + "provider_fst_outline", + "provider_fst_positive", + "pwid_negative", + "pwid_outline", + "pwid_positive", + "question_circle_negative", + "question_circle_outline", + "question_circle_positive", + "question_negative", + "question_outline", + "question_positive", + "question_triangle_negative", + "question_triangle_outline", + "question_triangle_positive", + "rdt_result_invalid_negative", + "rdt_result_invalid_outline", + "rdt_result_invalid_positive", + "rdt_result_mixed_invalid_negative", + "rdt_result_mixed_invalid_outline", + "rdt_result_mixed_invalid_positive", + "rdt_result_mixed_invalid_rectangular_negative", + "rdt_result_mixed_invalid_rectangular_outline", + "rdt_result_mixed_invalid_rectangular_positive", + "rdt_result_mixed_negative", + "rdt_result_mixed_outline", + "rdt_result_mixed_positive", + "rdt_result_mixed_rectangular_negative", + "rdt_result_mixed_rectangular_outline", + "rdt_result_mixed_rectangular_positive", + "rdt_result_neg_invalid_negative", + "rdt_result_neg_invalid_outline", + "rdt_result_neg_invalid_positive", + "rdt_result_neg_invalid_rectangular_negative", + "rdt_result_neg_invalid_rectangular_outline", + "rdt_result_neg_invalid_rectangular_positive", + "rdt_result_neg_negative", + "rdt_result_neg_outline", + "rdt_result_neg_positive", + "rdt_result_neg_rectangular_negative", + "rdt_result_neg_rectangular_outline", + "rdt_result_neg_rectangular_positive", + "rdt_result_negative_negative", + "rdt_result_negative_outline", + "rdt_result_negative_positive", + "rdt_result_no_test_negative", + "rdt_result_no_test_outline", + "rdt_result_no_test_positive", + "rdt_result_out_stock_negative", + "rdt_result_out_stock_outline", + "rdt_result_out_stock_positive", + "rdt_result_pf_invalid_negative", + "rdt_result_pf_invalid_outline", + "rdt_result_pf_invalid_positive", + "rdt_result_pf_invalid_rectangular_negative", + "rdt_result_pf_invalid_rectangular_outline", + "rdt_result_pf_invalid_rectangular_positive", + "rdt_result_pf_negative", + "rdt_result_pf_outline", + "rdt_result_pf_positive", + "rdt_result_pf_rectangular_negative", + "rdt_result_pf_rectangular_outline", + "rdt_result_pf_rectangular_positive", + "rdt_result_positive_negative", + "rdt_result_positive_outline", + "rdt_result_positive_positive", + "rdt_result_pv_invalid_negative", + "rdt_result_pv_invalid_outline", + "rdt_result_pv_invalid_positive", + "rdt_result_pv_invalid_rectangular_negative", + "rdt_result_pv_invalid_rectangular_outline", + "rdt_result_pv_invalid_rectangular_positive", + "rdt_result_pv_negative", + "rdt_result_pv_outline", + "rdt_result_pv_positive", + "rdt_result_pv_rectangular_negative", + "rdt_result_pv_rectangular_outline", + "rdt_result_pv_rectangular_positive", + "referral_negative", + "referral_outline", + "referral_positive", + "refused_negative", + "refused_outline", + "refused_positive", + "ribbon_negative", + "ribbon_outline", + "ribbon_positive", + "rmnh_negative", + "rmnh_outline", + "rmnh_positive", + "running_water_negative", + "running_water_outline", + "running_water_positive", + "rural_post_negative", + "rural_post_outline", + "rural_post_positive", + "sad_negative", + "sad_outline", + "sad_positive", + "sanitizer_negative", + "sanitizer_outline", + "sanitizer_positive", + "sayana_press_negative", + "sayana_press_outline", + "sayana_press_positive", + "security_worker_negative", + "security_worker_outline", + "security_worker_positive", + "sexual_reproductive_health_negative", + "sexual_reproductive_health_outline", + "sexual_reproductive_health_positive", + "small_plane_negative", + "small_plane_outline", + "small_plane_positive", + "social_distancing_negative", + "social_distancing_outline", + "social_distancing_positive", + "spraying_negative", + "spraying_outline", + "spraying_positive", + "square_large_negative", + "square_large_outline", + "square_large_positive", + "square_medium_negative", + "square_medium_outline", + "square_medium_positive", + "square_small_negative", + "square_small_outline", + "square_small_positive", + "star_large_negative", + "star_large_outline", + "star_large_positive", + "star_medium_negative", + "star_medium_outline", + "star_medium_positive", + "star_small_negative", + "star_small_outline", + "star_small_positive", + "stethoscope_negative", + "stethoscope_outline", + "stethoscope_positive", + "sti_negative", + "sti_outline", + "sti_positive", + "stock_out_negative", + "stock_out_outline", + "stock_out_positive", + "stop_negative", + "stop_outline", + "stop_positive", + "surgical_sterilization_negative", + "surgical_sterilization_outline", + "surgical_sterilization_positive", + "sweating_negative", + "sweating_outline", + "sweating_positive", + "symptom_negative", + "symptom_outline", + "symptom_positive", + "synergist_insecticide_bioassays_negative", + "synergist_insecticide_bioassays_outline", + "synergist_insecticide_bioassays_positive", + "syringe_negative", + "syringe_outline", + "syringe_positive", + "tac_negative", + "tac_outline", + "tac_positive", + "tb_negative", + "tb_outline", + "tb_positive", + "transgender_negative", + "transgender_outline", + "transgender_positive", + "traumatism_negative", + "traumatism_outline", + "traumatism_positive", + "travel_negative", + "travel_outline", + "travel_positive", + "treated_water_negative", + "treated_water_outline", + "treated_water_positive", + "triangle_large_negative", + "triangle_large_outline", + "triangle_large_positive", + "triangle_medium_negative", + "triangle_medium_outline", + "triangle_medium_positive", + "triangle_small_negative", + "triangle_small_outline", + "triangle_small_positive", + "truck_driver_negative", + "truck_driver_outline", + "truck_driver_positive", + "un_pave_road_negative", + "un_pave_road_outline", + "un_pave_road_positive", + "underweight_negative", + "underweight_outline", + "underweight_positive", + "vespa_motorcycle_negative", + "vespa_motorcycle_outline", + "vespa_motorcycle_positive", + "vih_aids_negative", + "vih_aids_outline", + "vih_aids_positive", + "virus_negative", + "virus_outline", + "virus_positive", + "vomiting_negative", + "vomiting_outline", + "vomiting_positive", + "war_negative", + "war_outline", + "war_positive", + "wash_hands_negative", + "wash_hands_outline", + "wash_hands_positive", + "water_sanitation_negative", + "water_sanitation_outline", + "water_sanitation_positive", + "water_treatment_negative", + "water_treatment_outline", + "water_treatment_positive", + "weight_negative", + "weight_outline", + "weight_positive", + "wold_care_negative", + "wold_care_outline", + "wold_care_positive", + "woman_negative", + "woman_outline", + "woman_positive", + "yes_negative", + "yes_outline", + "yes_positive", + "young_people_negative", + "young_people_outline", + "young_people_positive", + ) +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconDIModule.kt new file mode 100644 index 0000000000..8f0b5ab017 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconDIModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +internal class IconDIModule diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt new file mode 100644 index 0000000000..d95170bbd1 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload +import org.hisp.dhis.android.core.arch.call.factories.internal.UidsCallCoroutines +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.systeminfo.DHISVersion +import org.hisp.dhis.android.core.systeminfo.DHISVersionManager +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconCall( + private val handler: CustomIconHandler, + private val service: IconService, + private val dhis2VersionManager: DHISVersionManager, + private val apiDownloader: APIDownloader, +) : UidsCallCoroutines { + + companion object { + private const val MAX_UID_LIST_SIZE = 50 + } + + override suspend fun download(uids: Set): List { + return if (dhis2VersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + apiDownloader.downloadPartitioned( + uids, + MAX_UID_LIST_SIZE, + handler, + ) { partitionKeys: Set -> + try { + val customIcons = service.getCustomIcons( + CustomIconFields.allFields, + keys = CustomIconFields.key.`in`(partitionKeys), + ) + Payload(customIcons) + } catch (ignored: Exception) { + Payload() + } + } + } else { + emptyList() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt new file mode 100644 index 0000000000..144f0328e1 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.icon.CustomIcon + +internal object CustomIconFields { + private const val KEY = "key" + private const val FILE_RESOURCE_UID = "fileResourceUid" + private const val HREF = "href" + + private val fh = FieldsHelper() + + val key = fh.field(KEY) + + val allFields: Fields = + Fields.builder() + .fields( + fh.field(KEY), + fh.field(FILE_RESOURCE_UID), + fh.field(HREF), + ) + .build() +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconHandler.kt new file mode 100644 index 0000000000..052f236f8b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.ObjectWithoutUidHandlerImpl +import org.hisp.dhis.android.core.icon.CustomIcon +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconHandler( + store: CustomIconStore, +) : ObjectWithoutUidHandlerImpl(store) diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt new file mode 100644 index 0000000000..ea76a25b3b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.modules.internal.UntypedSuspendModuleDownloader +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconModuleDownloader internal constructor( + private val customIconCall: CustomIconCall, + private val customIconSeeker: CustomIconSeeker, + private val customIconStore: CustomIconStore, +) : UntypedSuspendModuleDownloader { + + override suspend fun downloadMetadata() { + val customIcons = customIconSeeker.seekUids() + val existingCustomIcons = customIconStore.selectStringColumnsWhereClause(CustomIconTableInfo.Columns.KEY, "1") + val customIconsToDownload = customIcons.minus(existingCustomIcons.toSet()) + customIconCall.download(customIconsToDownload) + + //TODO Clean non-existing icons + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt new file mode 100644 index 0000000000..b14f9defc9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.MultipleTableQueryBuilder +import org.hisp.dhis.android.core.arch.db.uidseeker.internal.BaseUidsSeeker +import org.hisp.dhis.android.core.common.NameableWithStyleColumns +import org.hisp.dhis.android.core.common.objectstyle.internal.TableWithObjectStyle +import org.hisp.dhis.android.core.icon.DefaultIcon +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconSeeker( + databaseAdapter: DatabaseAdapter, +) : BaseUidsSeeker(databaseAdapter) { + fun seekUids(): Set { + val query = MultipleTableQueryBuilder() + .generateQuery(NameableWithStyleColumns.ICON, TableWithObjectStyle.allTableNames).build() + + val allIcons = readSingleColumnResults(query) + + return allIcons.filterNot { DefaultIcon.all.contains(it)}.toSet() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt new file mode 100644 index 0000000000..050349c473 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectWithoutUidStore +import org.hisp.dhis.android.core.icon.CustomIcon + +internal interface CustomIconStore : ObjectWithoutUidStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt new file mode 100644 index 0000000000..47d123405e --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.WhereStatementBinder +import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectWithoutUidStoreImpl +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconStoreImpl( + databaseAdapter: DatabaseAdapter +) : CustomIconStore, + ObjectWithoutUidStoreImpl( + databaseAdapter, + CustomIconTableInfo.TABLE_INFO, + BINDER, + WHERE_UPDATE_BINDER, + WHERE_DELETE_BINDER, + CustomIcon::create, + ) { + + companion object { + private val BINDER = StatementBinder { o: CustomIcon, w: StatementWrapper -> + w.bind(1, o.key()) + w.bind(2, o.fileResourceUid()) + w.bind(3, o.href()) + } + + private val WHERE_UPDATE_BINDER = WhereStatementBinder { o: CustomIcon, w: StatementWrapper -> + w.bind(4, o.key()) + } + + private val WHERE_DELETE_BINDER = WhereStatementBinder { o: CustomIcon, w: StatementWrapper -> + w.bind(1, o.key()) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt new file mode 100644 index 0000000000..991dd1d72b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.hisp.dhis.android.core.wipe.internal.ModuleWiper +import org.hisp.dhis.android.core.wipe.internal.TableWiper +import org.koin.core.annotation.Singleton + +@Singleton +internal class IconModuleWiper( + private val tableWiper: TableWiper, +) : ModuleWiper { + + override fun wipeMetadata() { + tableWiper.wipeTable(CustomIconTableInfo.TABLE_INFO) + } + + override fun wipeData() { + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt new file mode 100644 index 0000000000..a0eb9e0c3b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.api.filters.internal.Filter +import org.hisp.dhis.android.core.arch.api.filters.internal.Where +import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.icon.CustomIcon +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface IconService { + + @GET(ICONS) + suspend fun getCustomIcons( + @Query("fields") @Which fields: Fields, + @Query("keys") @Where keys: Filter, + ): List + + companion object { + const val ICONS = "icons" + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/wipe/internal/D2ModuleWipers.kt b/core/src/main/java/org/hisp/dhis/android/core/wipe/internal/D2ModuleWipers.kt index 1c7be2a117..47b569a91e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/wipe/internal/D2ModuleWipers.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/wipe/internal/D2ModuleWipers.kt @@ -39,6 +39,7 @@ import org.hisp.dhis.android.core.enrollment.internal.EnrollmentModuleWiper import org.hisp.dhis.android.core.event.internal.EventModuleWiper import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemModuleWiper import org.hisp.dhis.android.core.fileresource.internal.FileResourceModuleWiper +import org.hisp.dhis.android.core.icon.internal.IconModuleWiper import org.hisp.dhis.android.core.imports.internal.ImportModuleWiper import org.hisp.dhis.android.core.indicator.internal.IndicatorModuleWiper import org.hisp.dhis.android.core.legendset.internal.LegendSetModuleWiper @@ -76,6 +77,7 @@ internal class D2ModuleWipers( event: EventModuleWiper, expressionDimensionItem: ExpressionDimensionItemModuleWiper, fileResource: FileResourceModuleWiper, + icon: IconModuleWiper, importModule: ImportModuleWiper, indicator: IndicatorModuleWiper, legendSet: LegendSetModuleWiper, @@ -114,6 +116,7 @@ internal class D2ModuleWipers( event, expressionDimensionItem, fileResource, + icon, importModule, indicator, legendSet, diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt new file mode 100644 index 0000000000..c37a7badf5 --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.data.icon + +import org.hisp.dhis.android.core.icon.CustomIcon + +object CustomIconSamples { + + fun getCustomIcon(): CustomIcon { + return CustomIcon.builder() + .id(1L) + .key("childIcon") + .fileResourceUid("lNrwSpIy1Q9") + .href("https://play.im.dhis2.org/dev/api/icons/childIcon/icon") + .build() + } +} diff --git a/core/src/sharedTest/resources/icon/custom_icon.json b/core/src/sharedTest/resources/icon/custom_icon.json new file mode 100644 index 0000000000..fe50d75a13 --- /dev/null +++ b/core/src/sharedTest/resources/icon/custom_icon.json @@ -0,0 +1,5 @@ +{ + "key": "childIcon", + "fileResourceUid": "lNrwSpIy1Q9", + "href": "https://play.im.dhis2.org/dev/api/icons/childIcon/icon" +} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt b/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt new file mode 100644 index 0000000000..4f32c4ce56 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class CustomIconShould : + BaseObjectShould("icon/custom_icon.json"), + ObjectShould { + + @Test + override fun map_from_json_string() { + val icon = objectMapper.readValue(jsonStream, CustomIcon::class.java) + + assertThat(icon.key()).isEqualTo("childIcon") + assertThat(icon.fileResourceUid()).isEqualTo("lNrwSpIy1Q9") + assertThat(icon.href()).isEqualTo("https://play.im.dhis2.org/dev/api/icons/childIcon/icon") + } +} From 273d9eed2617ca3d8e463cf8e00f58098b53bcbe Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 9 Feb 2024 13:38:57 +0100 Subject: [PATCH 086/222] [ANDROSDK-1630] Add icon payload in mock server --- .../hisp/dhis/android/core/icon/IconType.kt | 34 +++++++++++++++++++ .../core/icon/internal/CustomIconCall.kt | 4 ++- .../android/core/icon/internal/IconService.kt | 1 + .../core/mockwebserver/Dhis2MockServer.java | 4 +++ .../resources/icon/custom_icons.json | 12 +++++++ .../resources/program/program_stages.json | 2 +- .../resources/program/programs.json | 2 +- .../resources/systeminfo/system_info.json | 2 +- 8 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt create mode 100644 core/src/sharedTest/resources/icon/custom_icons.json diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt new file mode 100644 index 0000000000..bdc25352ba --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.icon + +enum class IconType { + CUSTOM, + DEFAULT, +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt index d95170bbd1..658b348686 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt @@ -32,6 +32,7 @@ import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.arch.call.factories.internal.UidsCallCoroutines import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.IconType import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.hisp.dhis.android.core.systeminfo.DHISVersionManager import org.koin.core.annotation.Singleton @@ -57,8 +58,9 @@ internal class CustomIconCall( ) { partitionKeys: Set -> try { val customIcons = service.getCustomIcons( - CustomIconFields.allFields, + fields = CustomIconFields.allFields, keys = CustomIconFields.key.`in`(partitionKeys), + type = IconType.CUSTOM.name ) Payload(customIcons) } catch (ignored: Exception) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt index a0eb9e0c3b..3d76a7c08c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt @@ -41,6 +41,7 @@ internal interface IconService { suspend fun getCustomIcons( @Query("fields") @Which fields: Fields, @Query("keys") @Where keys: Filter, + @Query("type") type: String?, ): List companion object { diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index 62e48eff2f..d740834080 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -103,6 +103,7 @@ public class Dhis2MockServer { private static final String NEW_EVENTS_JSON = "event/new_tracker_importer_events.json"; private static final String LEGEND_SETS_JSON = "legendset/legend_sets.json"; private static final String EXPRESSION_DIMENSION_ITEMS = "expressiondimensionitem/expression_dimension_items.json"; + private static final String CUSTOM_ICONS_JSON = "icon/custom_icons.json"; private static final String TRACKED_ENTITY_INSTANCES_JSON = "trackedentity/tracked_entity_instances.json"; private static final String NEW_TRACKED_ENTITY_INSTANCES_JSON = "trackedentity/new_tracker_importer_tracked_entities.json"; @@ -301,6 +302,8 @@ public MockResponse dispatch(RecordedRequest request) { return createMockResponse(LEGEND_SETS_JSON); } else if (path.startsWith("/api/expressionDimensionItems?")) { return createMockResponse(EXPRESSION_DIMENSION_ITEMS); + } else if (path.startsWith("/api/icons?")) { + return createMockResponse(CUSTOM_ICONS_JSON); } else if (path.startsWith("/api/trackedEntityAttributes/aejWyOfXge6/generateAndReserve")) { return createMockResponse(RESERVE_VALUES_JSON); } else if (path.startsWith("/api/metadata")) { @@ -385,6 +388,7 @@ public void enqueueMetadataResponses() { enqueueMockResponse(LEGEND_SETS_JSON); enqueueMockResponse(ATTRIBUTES_JSON); enqueueMockResponse(EXPRESSION_DIMENSION_ITEMS); + enqueueMockResponse(CUSTOM_ICONS_JSON); } private MockResponse createMockResponse(String fileName) { diff --git a/core/src/sharedTest/resources/icon/custom_icons.json b/core/src/sharedTest/resources/icon/custom_icons.json new file mode 100644 index 0000000000..10116e124b --- /dev/null +++ b/core/src/sharedTest/resources/icon/custom_icons.json @@ -0,0 +1,12 @@ +[ + { + "key": "antenatal_icon", + "fileResourceUid": "lNrwSpIy1Q9", + "href": "https://play.im.dhis2.org/dev/api/icons/antenatal_icon/icon" + }, + { + "key": "visit_icon", + "fileResourceUid": "yx5Vm0DBjFr", + "href": "https://play.im.dhis2.org/dev/api/icons/visit_icon/icon" + } +] \ No newline at end of file diff --git a/core/src/sharedTest/resources/program/program_stages.json b/core/src/sharedTest/resources/program/program_stages.json index 807cba967b..154645b314 100644 --- a/core/src/sharedTest/resources/program/program_stages.json +++ b/core/src/sharedTest/resources/program/program_stages.json @@ -32,7 +32,7 @@ "reportDateToUse": "report_date_to_use", "style": { "color": "#444", - "icon": "program-stage-icon" + "icon": "visit_icon" }, "access": { "read": true, diff --git a/core/src/sharedTest/resources/program/programs.json b/core/src/sharedTest/resources/program/programs.json index 0b130ffabe..294b76dce6 100644 --- a/core/src/sharedTest/resources/program/programs.json +++ b/core/src/sharedTest/resources/program/programs.json @@ -37,7 +37,7 @@ "maxTeiCountToReturn": 20, "style": { "color": "#333", - "icon": "program-icon" + "icon": "antenatal_icon" }, "access": { "data": { diff --git a/core/src/sharedTest/resources/systeminfo/system_info.json b/core/src/sharedTest/resources/systeminfo/system_info.json index 8a0a22cbd4..dfec116046 100644 --- a/core/src/sharedTest/resources/systeminfo/system_info.json +++ b/core/src/sharedTest/resources/systeminfo/system_info.json @@ -7,7 +7,7 @@ "lastAnalyticsTableSuccess": "2017-11-29T03:32:45.861", "intervalSinceLastAnalyticsTableSuccess": "7 h, 55 m, 1 s", "lastAnalyticsTableRuntime": "7 m, 40 s", - "version": "2.40.0", + "version": "2.41.0", "revision": "585e4bd", "buildTime": "2017-10-12T06:22:05.000", "jasperReportsVersion": "6.3.1", From 9614af2387d816b60280e36bc9a0f52fa9b85224 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 14 Feb 2024 12:59:27 +0100 Subject: [PATCH 087/222] [ANDROSDK-1630] Add CustomIcon collection cleaner --- .../internal/BaseCollectionCleaner.kt | 44 +++++++++++++++++++ .../internal/CollectionCleanerImpl.kt | 18 ++++---- .../arch/cleaners/internal/LinkCleanerImpl.kt | 17 ++++--- .../internal/CustomIconCollectionCleaner.kt | 42 ++++++++++++++++++ .../internal/CustomIconModuleDownloader.kt | 4 +- 5 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/BaseCollectionCleaner.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCollectionCleaner.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/BaseCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/BaseCollectionCleaner.kt new file mode 100644 index 0000000000..0fcc66220c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/BaseCollectionCleaner.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.cleaners.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper + +internal open class BaseCollectionCleaner( + private val tableName: String, + private val databaseAdapter: DatabaseAdapter, + private val key: String, +) { + fun deleteNotPresentByKey(uids: Collection): Boolean { + val objectUids = CollectionsHelper.commaAndSpaceSeparatedCollectionValues(uids.map { "'$it'" }) + val clause = "$key NOT IN ($objectUids);" + return databaseAdapter.delete(tableName, clause, null) > 0 + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/CollectionCleanerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/CollectionCleanerImpl.kt index e4f069bcf7..73e354cb38 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/CollectionCleanerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/CollectionCleanerImpl.kt @@ -28,25 +28,27 @@ package org.hisp.dhis.android.core.arch.cleaners.internal import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter -import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper import org.hisp.dhis.android.core.common.IdentifiableColumns import org.hisp.dhis.android.core.common.ObjectWithUidInterface internal open class CollectionCleanerImpl

( - private val tableName: String, - private val databaseAdapter: DatabaseAdapter, -) : CollectionCleaner

{ + tableName: String, + databaseAdapter: DatabaseAdapter, +) : CollectionCleaner

, + BaseCollectionCleaner( + tableName = tableName, + databaseAdapter = databaseAdapter, + key = IdentifiableColumns.UID, + ) { override fun deleteNotPresent(objects: Collection

?): Boolean { if (objects == null) { return false } - return deleteNotPresentByUid(objects.map { it.uid() }) + return deleteNotPresentByKey(objects.map { it.uid() }) } override fun deleteNotPresentByUid(uids: Collection): Boolean { - val objectUids = CollectionsHelper.commaAndSpaceSeparatedCollectionValues(uids.map { "'$it'" }) - val clause = IdentifiableColumns.UID + " NOT IN (" + objectUids + ");" - return databaseAdapter.delete(tableName, clause, null) > 0 + return deleteNotPresentByKey(uids) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt index 81fc9f81dd..ccfbe5a7c4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt @@ -33,19 +33,22 @@ import org.hisp.dhis.android.core.arch.helpers.UidsHelper.commaSeparatedUidsWith import org.hisp.dhis.android.core.common.ObjectWithUidInterface internal open class LinkCleanerImpl

( - private val tableName: String, - private val applicableColumn: String, + tableName: String, + applicableColumn: String, + databaseAdapter: DatabaseAdapter, private val parentStore: ObjectStore

, - private val databaseAdapter: DatabaseAdapter, -) : LinkCleaner

{ +) : LinkCleaner

, + BaseCollectionCleaner( + tableName = tableName, + databaseAdapter = databaseAdapter, + key = applicableColumn, + ){ override fun deleteNotPresent(objects: Collection

?): Boolean { if (objects == null) { return false } - val objectUids = commaSeparatedUidsWithSingleQuotationMarks(objects) - val clause = "$applicableColumn NOT IN ($objectUids);" - return databaseAdapter.delete(tableName, clause, null) > 0 + return deleteNotPresentByKey(objects.map { it.uid() }) } override fun deleteNotPresentInDb(): Boolean { diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCollectionCleaner.kt new file mode 100644 index 0000000000..a5da20f7d0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCollectionCleaner.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.arch.cleaners.internal.BaseCollectionCleaner +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class CustomIconCollectionCleaner( + databaseAdapter: DatabaseAdapter, +) : BaseCollectionCleaner( + tableName = CustomIconTableInfo.TABLE_INFO.name(), + databaseAdapter = databaseAdapter, + key = CustomIconTableInfo.Columns.KEY, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt index ea76a25b3b..085bde78f8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconModuleDownloader.kt @@ -36,6 +36,7 @@ internal class CustomIconModuleDownloader internal constructor( private val customIconCall: CustomIconCall, private val customIconSeeker: CustomIconSeeker, private val customIconStore: CustomIconStore, + private val customIconCleaner: CustomIconCollectionCleaner, ) : UntypedSuspendModuleDownloader { override suspend fun downloadMetadata() { @@ -43,7 +44,6 @@ internal class CustomIconModuleDownloader internal constructor( val existingCustomIcons = customIconStore.selectStringColumnsWhereClause(CustomIconTableInfo.Columns.KEY, "1") val customIconsToDownload = customIcons.minus(existingCustomIcons.toSet()) customIconCall.download(customIconsToDownload) - - //TODO Clean non-existing icons + customIconCleaner.deleteNotPresentByKey(customIcons) } } From e65c487520088ef55e9398f5476d8b8979cf8ac7 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 14 Feb 2024 16:05:59 +0100 Subject: [PATCH 088/222] [ANDROSDK-1630] Download custom icons --- ...aseFileResourceRoutineIntegrationShould.kt | 2 ++ .../internal/FileResourceRoutineShould.kt | 1 + .../SystemInfoModuleMockIntegrationShould.kt | 4 +-- ...ectionRepositoryMockIntegrationShould.java | 4 +-- .../MaintenanceMockIntegrationShould.java | 2 +- ...ectionRepositoryMockIntegrationShould.java | 2 +- ...ectionRepositoryMockIntegrationShould.java | 2 +- .../core/fileresource/FileResourceDomain.kt | 2 ++ .../fileresource/FileResourceDownloadConst.kt | 1 + .../core/fileresource/FileResourceRoutine.kt | 10 +++++-- .../internal/FileResourceDownloadCall.kt | 23 +++++++++++++- .../FileResourceDownloadCallHelper.kt | 13 ++++++++ .../internal/FileResourceDownloadParams.kt | 6 ++-- .../internal/FileResourceHelper.kt | 2 ++ .../internal/FileResourceService.kt | 7 +++++ .../core/icon/internal/CustomIconCall.kt | 8 ++--- .../core/icon/internal/CustomIconFields.kt | 2 -- .../android/core/icon/internal/IconService.kt | 6 ++-- .../core/data/program/ProgramSamples.java | 2 +- .../data/systeminfo/SystemInfoSamples.java | 2 +- .../resources/icon/custom_icons.json | 30 ++++++++++++------- .../domain/metadata/MetadataCallShould.kt | 6 ++++ .../core/systeminfo/SystemInfoShould.kt | 2 +- 23 files changed, 104 insertions(+), 35 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/BaseFileResourceRoutineIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/BaseFileResourceRoutineIntegrationShould.kt index 680e10ecad..4fa5bb59aa 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/BaseFileResourceRoutineIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/BaseFileResourceRoutineIntegrationShould.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.fileresource.internal import org.hisp.dhis.android.core.category.internal.CategoryComboStoreImpl import org.hisp.dhis.android.core.dataelement.internal.DataElementStoreImpl import org.hisp.dhis.android.core.event.internal.EventStoreImpl +import org.hisp.dhis.android.core.icon.internal.CustomIconStoreImpl import org.hisp.dhis.android.core.option.internal.OptionSetStoreImpl import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitStoreImpl import org.hisp.dhis.android.core.program.internal.ProgramStageStoreImpl @@ -51,6 +52,7 @@ internal open class BaseFileResourceRoutineIntegrationShould : BaseMockIntegrati protected val trackedEntityDataValueStore = TrackedEntityDataValueStoreImpl(databaseAdapter) protected val trackedEntityAttributeValueStore = TrackedEntityAttributeValueStoreImpl(databaseAdapter) protected val fileResourceStore = FileResourceStoreImpl(d2.databaseAdapter()) + protected val customIconStore = CustomIconStoreImpl(d2.databaseAdapter()) private val optionSetStore = OptionSetStoreImpl(d2.databaseAdapter()) // Metadata stores diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceRoutineShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceRoutineShould.kt index 9cf8b5a7e6..a77ac8c8ef 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceRoutineShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceRoutineShould.kt @@ -44,6 +44,7 @@ internal class FileResourceRoutineShould : BaseFileResourceRoutineIntegrationSho dataValueCollectionRepository = d2.dataValueModule().dataValues(), fileResourceCollectionRepository = d2.fileResourceModule().fileResources(), fileResourceStore = fileResourceStore, + customIconStore = customIconStore, trackedEntityAttributeCollectionRepository = d2.trackedEntityModule().trackedEntityAttributes(), trackedEntityAttributeValueCollectionRepository = d2.trackedEntityModule().trackedEntityAttributeValues(), trackedEntityDataValueCollectionRepository = d2.trackedEntityModule().trackedEntityDataValues(), diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/systeminfo/SystemInfoModuleMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/systeminfo/SystemInfoModuleMockIntegrationShould.kt index 171a05913f..ae73aa6289 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/systeminfo/SystemInfoModuleMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/systeminfo/SystemInfoModuleMockIntegrationShould.kt @@ -35,13 +35,13 @@ class SystemInfoModuleMockIntegrationShould : BaseMockIntegrationTestFullDispatc @Test fun allow_access_to_system_info_user() { val systemInfo = d2.systemInfoModule().systemInfo().blockingGet()!! - assertThat(systemInfo.version()).isEqualTo("2.40.0") + assertThat(systemInfo.version()).isEqualTo("2.41.0") assertThat(systemInfo.systemName()).isEqualTo("DHIS 2 Demo - Sierra Leone") } @Test fun allow_access_to_version_manager() { val version = d2.systemInfoModule().versionManager().getVersion() - assertThat(version).isEqualTo(DHISVersion.V2_40) + assertThat(version).isEqualTo(DHISVersion.V2_41) } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/D2ErrorCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/D2ErrorCollectionRepositoryMockIntegrationShould.java index be6983ecef..9ec810760b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/D2ErrorCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/D2ErrorCollectionRepositoryMockIntegrationShould.java @@ -67,7 +67,7 @@ public void filter_d2_error_by_d2_error_code() { public void filter_d2_error_by_d2_error_component() { List d2Errors = d2.maintenanceModule().d2Errors() .byD2ErrorComponent().eq(D2ErrorComponent.Server).blockingGet(); - assertThat(d2Errors.size()).isEqualTo(1); + assertThat(d2Errors.size()).isEqualTo(3); } @Test @@ -105,6 +105,6 @@ public void filter_d2_error_by_created() { List d2Errors = d2.maintenanceModule().d2Errors() .byCreated().inPeriods(Lists.newArrayList(todayPeriod)).blockingGet(); - assertThat(d2Errors.size()).isEqualTo(2); + assertThat(d2Errors.size()).isEqualTo(4); } } \ No newline at end of file diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/MaintenanceMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/MaintenanceMockIntegrationShould.java index 0159095b4f..38c770f359 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/MaintenanceMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/maintenance/MaintenanceMockIntegrationShould.java @@ -83,6 +83,6 @@ public void get_vulnerabilities_for_low_threshold() { @Test public void allow_access_to_d2_errors() { List d2Errors = d2.maintenanceModule().d2Errors().blockingGet(); - assertThat(d2Errors.size()).isEqualTo(2); + assertThat(d2Errors.size()).isEqualTo(4); } } \ No newline at end of file diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java index 29c9863901..1f2af2fde7 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java @@ -254,7 +254,7 @@ public void filter_by_field_color() { @Test public void filter_by_field_icon() { List programs = d2.programModule().programs() - .byIcon().eq("program-icon") + .byIcon().eq("antenatal_icon") .blockingGet(); assertThat(programs.size()).isEqualTo(1); } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java index e8114b0319..f6774168e7 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramStageCollectionRepositoryMockIntegrationShould.java @@ -361,7 +361,7 @@ public void filter_by_field_color() { @Test public void filter_by_field_icon() { List programStages = d2.programModule().programStages() - .byIcon().eq("program-stage-icon") + .byIcon().eq("visit_icon") .blockingGet(); assertThat(programStages.size()).isEqualTo(1); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt index 3ef3dae1c3..cf931e9569 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt @@ -34,4 +34,6 @@ enum class FileResourceDomain { MESSAGE_ATTACHMENT, USER_AVATAR, ORG_UNIT, + CUSTOM_ICON, + JOB_DATA, } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt index d1a024be63..5e15aaaf97 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt @@ -42,4 +42,5 @@ enum class FileResourceElementType { enum class FileResourceDomainType { AGGREGATED, TRACKER, + CUSTOM_ICON, } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt index 3de9eede42..33d7e91c5d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt @@ -35,6 +35,8 @@ import org.hisp.dhis.android.core.dataelement.DataElementCollectionRepository import org.hisp.dhis.android.core.datavalue.DataValue import org.hisp.dhis.android.core.datavalue.DataValueCollectionRepository import org.hisp.dhis.android.core.fileresource.internal.FileResourceStore +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.internal.CustomIconStore import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeCollectionRepository import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue @@ -54,6 +56,7 @@ internal class FileResourceRoutine( private val trackedEntityAttributeCollectionRepository: TrackedEntityAttributeCollectionRepository, private val trackedEntityDataValueCollectionRepository: TrackedEntityDataValueCollectionRepository, private val fileResourceStore: FileResourceStore, + private val customIconStore: CustomIconStore, private val trackedEntityAttributeValueCollectionRepository: TrackedEntityAttributeValueCollectionRepository, ) { fun deleteOutdatedFileResources(after: Date? = null): Completable { @@ -84,16 +87,19 @@ internal class FileResourceRoutine( .byDataElementUid().`in`(dataElementsUids) .blockingGet() + val customIcons = customIconStore.selectAll() + val fileResourceUids = dataValues.map(DataValue::value) + trackedEntityAttributeValues.map(TrackedEntityAttributeValue::value) + - trackedEntityDataValues.map(TrackedEntityDataValue::value) + trackedEntityDataValues.map(TrackedEntityDataValue::value) + + customIcons.map(CustomIcon::fileResourceUid) val calendar = Calendar.getInstance().apply { add(Calendar.HOUR_OF_DAY, -2) } val fileResources = fileResourceCollectionRepository .byUid().notIn(fileResourceUids.mapNotNull { it }) - .byDomain().eq(FileResourceDomain.DATA_VALUE) + .byDomain().`in`(FileResourceDomain.DATA_VALUE, FileResourceDomain.CUSTOM_ICON) .byLastUpdated().before(after ?: calendar.time) .blockingGet() diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt index 4267623b55..7796d30531 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt @@ -43,6 +43,7 @@ import org.hisp.dhis.android.core.fileresource.FileResourceDomainType import org.hisp.dhis.android.core.fileresource.FileResourceElementType import org.hisp.dhis.android.core.fileresource.FileResourceInternalAccessor import org.hisp.dhis.android.core.fileresource.FileResourceRoutine +import org.hisp.dhis.android.core.icon.CustomIcon import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.internal.SynchronizationSettingStore import org.koin.core.annotation.Singleton @@ -61,7 +62,7 @@ internal class FileResourceDownloadCall( ) { fun download(params: FileResourceDownloadParams): Flow = flow { - val progressManager = D2ProgressManager(2) + val progressManager = D2ProgressManager(4) val existingFileResources = fileResourceStore.selectUids() val paramsWithCorrectedMaxContentLength = params.copy( @@ -75,7 +76,12 @@ internal class FileResourceDownloadCall( downloadTrackerValues(paramsWithCorrectedMaxContentLength, existingFileResources) emit(progressManager.increaseProgress(FileResource::class.java, isComplete = false)) + + downloadCustomIcons(paramsWithCorrectedMaxContentLength, existingFileResources) + emit(progressManager.increaseProgress(FileResource::class.java, isComplete = false)) + fileResourceRoutine.blockingDeleteOutdatedFileResources() + emit(progressManager.increaseProgress(FileResource::class.java, isComplete = true)) } private suspend fun downloadAggregatedValues( @@ -151,6 +157,21 @@ internal class FileResourceDownloadCall( } } + private suspend fun downloadCustomIcons(params: FileResourceDownloadParams, existingFileResources: List) { + if (params.domainTypes.contains(FileResourceDomainType.CUSTOM_ICON)) { + val iconKeys: List = helper.getMissingCustomIcons(existingFileResources) + + downloadAndPersistFiles( + values = iconKeys, + maxContentLength = params.maxContentLength, + download = { v -> + fileResourceService.getCustomIcon(v.href()) + }, + getUid = { v -> v.fileResourceUid() }, + ) + } + } + private suspend fun downloadAndPersistFiles( values: List, maxContentLength: Int?, diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt index 0061c75476..51d44cbc3a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt @@ -34,6 +34,9 @@ import org.hisp.dhis.android.core.datavalue.DataValue import org.hisp.dhis.android.core.datavalue.DataValueTableInfo import org.hisp.dhis.android.core.datavalue.internal.DataValueStore import org.hisp.dhis.android.core.fileresource.FileResourceValueType +import org.hisp.dhis.android.core.icon.CustomIcon +import org.hisp.dhis.android.core.icon.CustomIconTableInfo +import org.hisp.dhis.android.core.icon.internal.CustomIconStore import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.hisp.dhis.android.core.systeminfo.DHISVersionManager import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeTableInfo @@ -52,6 +55,7 @@ internal class FileResourceDownloadCallHelper( private val trackedEntityAttributeStore: TrackedEntityAttributeStore, private val trackedEntityDataValueStore: TrackedEntityDataValueStore, private val dataValueStore: DataValueStore, + private val customIconStore: CustomIconStore, private val dhisVersionManager: DHISVersionManager, ) { @@ -117,4 +121,13 @@ internal class FileResourceDownloadCallHelper( .build() return dataValueStore.selectWhere(dataValuesWhereClause) } + + fun getMissingCustomIcons( + existingFileResources: List, + ): List { + val customIconsWhereClause = WhereClauseBuilder() + .appendNotInKeyStringValues(CustomIconTableInfo.Columns.FILE_RESOURCE_UID, existingFileResources) + .build() + return customIconStore.selectWhere(customIconsWhereClause) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt index 58c9c2c060..5fcbb2575d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt @@ -33,8 +33,8 @@ import org.hisp.dhis.android.core.fileresource.FileResourceElementType import org.hisp.dhis.android.core.fileresource.FileResourceValueType internal data class FileResourceDownloadParams( - val valueTypes: List = FileResourceValueType.values().asList(), - val elementTypes: List = FileResourceElementType.values().asList(), - val domainTypes: List = FileResourceDomainType.values().asList(), + val valueTypes: List = FileResourceValueType.entries, + val elementTypes: List = FileResourceElementType.entries, + val domainTypes: List = FileResourceDomainType.entries, val maxContentLength: Int? = null, ) : BaseScope diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt index 012f4989d1..4a631c38b4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt @@ -138,6 +138,8 @@ internal class FileResourceHelper( FileResourceDomainType.AGGREGATED -> getRelatedDataValue(fileResourceUid)?.syncState() ?: State.TO_POST + FileResourceDomainType.CUSTOM_ICON -> + State.SYNCED } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt index 6691dd86ab..56f481b37c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.fileresource.internal import okhttp3.MultipartBody import okhttp3.ResponseBody import org.hisp.dhis.android.core.fileresource.FileResource +import org.hisp.dhis.android.core.map.layer.internal.bing.BingServerResponse import retrofit2.http.* internal interface FileResourceService { @@ -61,6 +62,11 @@ internal interface FileResourceService { @Query("dimension") dimension: String, ): ResponseBody + @GET + suspend fun getCustomIcon( + @Url customIconHref: String, + ): ResponseBody + @GET("$DATA_VALUES/files") suspend fun getFileFromDataValue( @Query("de") dataElement: String, @@ -70,6 +76,7 @@ internal interface FileResourceService { @Query("dimension") dimension: String, ): ResponseBody + companion object { const val FILE_RESOURCES = "fileResources" const val FILE_RESOURCE = "fileResource" diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt index 658b348686..1b9294e8dd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconCall.kt @@ -57,12 +57,12 @@ internal class CustomIconCall( handler, ) { partitionKeys: Set -> try { - val customIcons = service.getCustomIcons( + service.getCustomIcons( fields = CustomIconFields.allFields, - keys = CustomIconFields.key.`in`(partitionKeys), - type = IconType.CUSTOM.name + keys = partitionKeys.joinToString(","), + type = IconType.CUSTOM.name, + paging = false, ) - Payload(customIcons) } catch (ignored: Exception) { Payload() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt index 144f0328e1..d59ea88b1d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt @@ -38,8 +38,6 @@ internal object CustomIconFields { private val fh = FieldsHelper() - val key = fh.field(KEY) - val allFields: Fields = Fields.builder() .fields( diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt index 3d76a7c08c..c07ca26651 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt @@ -31,6 +31,7 @@ import org.hisp.dhis.android.core.arch.api.fields.internal.Fields import org.hisp.dhis.android.core.arch.api.filters.internal.Filter import org.hisp.dhis.android.core.arch.api.filters.internal.Where import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.icon.CustomIcon import retrofit2.http.GET import retrofit2.http.Query @@ -40,9 +41,10 @@ internal interface IconService { @GET(ICONS) suspend fun getCustomIcons( @Query("fields") @Which fields: Fields, - @Query("keys") @Where keys: Filter, + @Query("keys") keys: String, @Query("type") type: String?, - ): List + @Query("paging") paging: Boolean, + ): Payload companion object { const val ICONS = "icons" diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java index 4ed5a88076..594b273ca4 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java @@ -111,7 +111,7 @@ public static Program getAntenatalProgram() { .categoryCombo(ObjectWithUid.create("m2jTvAj5kkm")) .access(Access.create(true, true, DataAccess.create(true, true))) .accessLevel(AccessLevel.PROTECTED) - .style(ObjectStyle.builder().color("#333").icon("program-icon").build()) + .style(ObjectStyle.builder().color("#333").icon("antenatal_icon").build()) .relatedProgram(ObjectWithUid.create("lxAQ7Zs9VYR")) .expiryDays(2) .completeEventsExpiryDays(4) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/systeminfo/SystemInfoSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/systeminfo/SystemInfoSamples.java index ad1603b1f3..bfa3537417 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/systeminfo/SystemInfoSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/systeminfo/SystemInfoSamples.java @@ -41,7 +41,7 @@ public static SystemInfo get1() { .id(1L) .serverDate(getDate("2017-11-29T11:27:46.935")) .dateFormat("yyyy-mm-dd") - .version("2.40.0") + .version("2.41.0") .contextPath("https://play.dhis2.org/android-current") .systemName("DHIS 2 Demo - Sierra Leone") .build(); diff --git a/core/src/sharedTest/resources/icon/custom_icons.json b/core/src/sharedTest/resources/icon/custom_icons.json index 10116e124b..8691b85e92 100644 --- a/core/src/sharedTest/resources/icon/custom_icons.json +++ b/core/src/sharedTest/resources/icon/custom_icons.json @@ -1,12 +1,20 @@ -[ - { - "key": "antenatal_icon", - "fileResourceUid": "lNrwSpIy1Q9", - "href": "https://play.im.dhis2.org/dev/api/icons/antenatal_icon/icon" +{ + "pager": { + "page": 1, + "pageCount": 1, + "total": 2, + "pageSize": 50 }, - { - "key": "visit_icon", - "fileResourceUid": "yx5Vm0DBjFr", - "href": "https://play.im.dhis2.org/dev/api/icons/visit_icon/icon" - } -] \ No newline at end of file + "icons": [ + { + "key": "antenatal_icon", + "fileResourceUid": "lNrwSpIy1Q9", + "href": "https://play.im.dhis2.org/dev/api/icons/antenatal_icon/icon" + }, + { + "key": "visit_icon", + "fileResourceUid": "yx5Vm0DBjFr", + "href": "https://play.im.dhis2.org/dev/api/icons/visit_icon/icon" + } + ] +} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt index 6e63293cd0..7b002f467e 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallShould.kt @@ -44,6 +44,7 @@ import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManage import org.hisp.dhis.android.core.constant.internal.ConstantModuleDownloader import org.hisp.dhis.android.core.dataset.internal.DataSetModuleDownloader import org.hisp.dhis.android.core.expressiondimensionitem.internal.ExpressionDimensionItemModuleDownloader +import org.hisp.dhis.android.core.icon.internal.CustomIconModuleDownloader import org.hisp.dhis.android.core.indicator.internal.IndicatorModuleDownloader import org.hisp.dhis.android.core.legendset.internal.LegendSetModuleDownloader import org.hisp.dhis.android.core.maintenance.D2Error @@ -94,6 +95,7 @@ class MetadataCallShould : BaseCallShould() { private val legendSetModuleDownloader: LegendSetModuleDownloader = mock() private val attributeModuleDownloader: AttributeModuleDownloader = mock() private val expressionDimensIndicatorModuleDownloader: ExpressionDimensionItemModuleDownloader = mock() + private val customIconDownloader: CustomIconModuleDownloader = mock() private val networkError: D2Error = D2Error.builder() .errorCode(D2ErrorCode.UNKNOWN_HOST) @@ -159,6 +161,9 @@ class MetadataCallShould : BaseCallShould() { categoryDownloader.stub { onBlocking { downloadMetadata() }.doReturn(Unit) } + customIconDownloader.stub { + onBlocking { downloadMetadata() }.doReturn(Unit) + } whenever(smsModule.configCase()).thenReturn(configCase) configCase.stub { onBlocking { refreshMetadataIdsCallable() }.doReturn(Unit) @@ -190,6 +195,7 @@ class MetadataCallShould : BaseCallShould() { legendSetModuleDownloader, attributeModuleDownloader, expressionDimensIndicatorModuleDownloader, + customIconDownloader, ) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SystemInfoShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SystemInfoShould.kt index 71d0e3f45c..ed315111e6 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SystemInfoShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SystemInfoShould.kt @@ -41,7 +41,7 @@ class SystemInfoShould : BaseObjectShould("systeminfo/system_info.json"), Object assertThat(systemInfo.serverDate()).isEqualTo(DateUtils.DATE_FORMAT.parse("2017-11-29T11:27:46.935")) assertThat(systemInfo.dateFormat()).isEqualTo("yyyy-mm-dd") - assertThat(systemInfo.version()).isEqualTo("2.40.0") + assertThat(systemInfo.version()).isEqualTo("2.41.0") assertThat(systemInfo.contextPath()).isEqualTo("https://play.dhis2.org/android-current") assertThat(systemInfo.systemName()).isEqualTo("DHIS 2 Demo - Sierra Leone") } From 5a93410139748194b81293fedccf994aa8671748 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 15 Feb 2024 10:52:01 +0100 Subject: [PATCH 089/222] [ANDROSDK-1630] Expose IconCollectionRepository --- .../MetadataCallMockIntegrationShould.kt | 2 +- ...llectionRepositoryMockIntegrationShould.kt | 69 +++++++++++++++++++ .../java/org/hisp/dhis/android/core/D2.kt | 5 ++ .../arch/api/internal/ServicesDIModule.kt | 2 +- .../arch/cleaners/internal/LinkCleanerImpl.kt | 3 +- .../core/arch/d2/internal/D2Modules.kt | 2 + .../internal/TableWithObjectStyle.kt | 4 +- .../internal/FileResourceDownloadCall.kt | 2 +- .../internal/FileResourceService.kt | 2 - .../dhis/android/core/icon/DefaultIcon.kt | 6 +- .../org/hisp/dhis/android/core/icon/Icon.kt | 38 ++++++++++ .../core/icon/IconCollectionRepository.kt | 69 +++++++++++++++++++ .../hisp/dhis/android/core/icon/IconModule.kt | 32 +++++++++ .../hisp/dhis/android/core/icon/IconType.kt | 2 +- .../core/icon/internal/CustomIconSeeker.kt | 2 +- .../core/icon/internal/CustomIconStore.kt | 4 +- .../core/icon/internal/CustomIconStoreImpl.kt | 11 ++- .../core/icon/internal/IconModuleImpl.kt | 41 +++++++++++ .../core/icon/internal/IconModuleWiper.kt | 1 + .../android/core/icon/internal/IconService.kt | 2 - 20 files changed, 283 insertions(+), 16 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/testapp/icon/IconCollectionRepositoryMockIntegrationShould.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/Icon.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/IconModule.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleImpl.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt index 8faf33711f..9b9bf758e5 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/domain/metadata/MetadataCallMockIntegrationShould.kt @@ -95,7 +95,7 @@ class MetadataCallMockIntegrationShould : BaseMockIntegrationTestEmptyDispatcher LegendSet::class, Attribute::class, ExpressionDimensionItem::class, - CustomIcon::class + CustomIcon::class, ).map { it.java.simpleName }, ) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/icon/IconCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/icon/IconCollectionRepositoryMockIntegrationShould.kt new file mode 100644 index 0000000000..47fcdabc93 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/icon/IconCollectionRepositoryMockIntegrationShould.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.testapp.icon + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.icon.Icon +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.junit.Assert.fail +import org.junit.Test + +class IconCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTestFullDispatcher() { + @Test + fun find_default_icon() { + val icon = d2.iconModule().icons() + .key("2g_negative") + .blockingGet() + + when (icon) { + is Icon.Default -> + assertThat(icon.key).isEqualTo("2g_negative") + + else -> + fail("Unexpected icon type") + } + } + + @Test + fun find_custom_icon() { + val icon = d2.iconModule().icons() + .key("antenatal_icon") + .blockingGet() + + when (icon) { + is Icon.Custom -> { + assertThat(icon.key).isEqualTo("antenatal_icon") + assertThat(icon.path).isNull() + } + + else -> + fail("Unexpected icon type") + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/D2.kt b/core/src/main/java/org/hisp/dhis/android/core/D2.kt index 29b5575227..c15ec42296 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/D2.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/D2.kt @@ -47,6 +47,7 @@ import org.hisp.dhis.android.core.enrollment.EnrollmentModule import org.hisp.dhis.android.core.event.EventModule import org.hisp.dhis.android.core.expressiondimensionitem.ExpressionDimensionItemModule import org.hisp.dhis.android.core.fileresource.FileResourceModule +import org.hisp.dhis.android.core.icon.IconModule import org.hisp.dhis.android.core.imports.internal.ImportModule import org.hisp.dhis.android.core.indicator.IndicatorModule import org.hisp.dhis.android.core.legendset.LegendSetModule @@ -169,6 +170,10 @@ class D2 internal constructor(internal val d2DIComponent: D2DIComponent) { return modules.fileResource } + fun iconModule(): IconModule { + return modules.icon + } + fun importModule(): ImportModule { return modules.importModule } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt index c7de8738ef..9e8a124b4f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/internal/ServicesDIModule.kt @@ -73,7 +73,7 @@ internal val servicesDIModule = module { single { get().create(ExpressionDimensionItemService::class.java) } single { get().create(ExternalMapLayerService::class.java) } single { get().create(FileResourceService::class.java) } - single { get().create(IconService::class.java)} + single { get().create(IconService::class.java) } single { get().create(IndicatorService::class.java) } single { get().create(IndicatorTypeService::class.java) } single { get().create(LegendSetService::class.java) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt index ccfbe5a7c4..9fc22f79f9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/cleaners/internal/LinkCleanerImpl.kt @@ -29,7 +29,6 @@ package org.hisp.dhis.android.core.arch.cleaners.internal import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectStore -import org.hisp.dhis.android.core.arch.helpers.UidsHelper.commaSeparatedUidsWithSingleQuotationMarks import org.hisp.dhis.android.core.common.ObjectWithUidInterface internal open class LinkCleanerImpl

( @@ -42,7 +41,7 @@ internal open class LinkCleanerImpl

( tableName = tableName, databaseAdapter = databaseAdapter, key = applicableColumn, - ){ + ) { override fun deleteNotPresent(objects: Collection

?): Boolean { if (objects == null) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2Modules.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2Modules.kt index d91afe6f78..5f553d1a76 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2Modules.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2Modules.kt @@ -39,6 +39,7 @@ import org.hisp.dhis.android.core.enrollment.EnrollmentModule import org.hisp.dhis.android.core.event.EventModule import org.hisp.dhis.android.core.expressiondimensionitem.ExpressionDimensionItemModule import org.hisp.dhis.android.core.fileresource.FileResourceModule +import org.hisp.dhis.android.core.icon.IconModule import org.hisp.dhis.android.core.imports.internal.ImportModule import org.hisp.dhis.android.core.indicator.IndicatorModule import org.hisp.dhis.android.core.legendset.LegendSetModule @@ -76,6 +77,7 @@ internal class D2Modules( val expressionDimensionItem: ExpressionDimensionItemModule, val fileResource: FileResourceModule, val importModule: ImportModule, + val icon: IconModule, val indicator: IndicatorModule, val legendSet: LegendSetModule, val dataStore: DataStoreModule, diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt index 5f303c5391..535561ae65 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt @@ -40,7 +40,7 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceFilterTable import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeTableInfo internal object TableWithObjectStyle { - //TODO Test table containing icon + // TODO Test table containing icon val allTableNames: List = setOf( DataElementTableInfo.TABLE_INFO, DataSetTableInfo.TABLE_INFO, @@ -51,7 +51,7 @@ internal object TableWithObjectStyle { ProgramTableInfo.TABLE_INFO, TrackedEntityAttributeTableInfo.TABLE_INFO, TrackedEntityInstanceFilterTableInfo.TABLE_INFO, - TrackedEntityTypeTableInfo.TABLE_INFO + TrackedEntityTypeTableInfo.TABLE_INFO, ).map { it.name() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt index 7796d30531..6e87b943c1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt @@ -48,7 +48,7 @@ import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.internal.SynchronizationSettingStore import org.koin.core.annotation.Singleton -@SuppressWarnings("LongParameterList") +@SuppressWarnings("LongParameterList", "MagicNumber") @Singleton internal class FileResourceDownloadCall( private val fileResourceStore: FileResourceStore, diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt index 56f481b37c..fde6358bc0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceService.kt @@ -30,7 +30,6 @@ package org.hisp.dhis.android.core.fileresource.internal import okhttp3.MultipartBody import okhttp3.ResponseBody import org.hisp.dhis.android.core.fileresource.FileResource -import org.hisp.dhis.android.core.map.layer.internal.bing.BingServerResponse import retrofit2.http.* internal interface FileResourceService { @@ -76,7 +75,6 @@ internal interface FileResourceService { @Query("dimension") dimension: String, ): ResponseBody - companion object { const val FILE_RESOURCES = "fileResources" const val FILE_RESOURCE = "fileResource" diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt index 707bbc1eb5..adf09fb292 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/DefaultIcon.kt @@ -28,7 +28,11 @@ package org.hisp.dhis.android.core.icon +@Suppress("LargeClass") internal object DefaultIcon { + + const val mimeType = "application/xml" + val all = listOf( "2g_negative", "2g_outline", @@ -928,4 +932,4 @@ internal object DefaultIcon { "young_people_outline", "young_people_positive", ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/Icon.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/Icon.kt new file mode 100644 index 0000000000..52116bf2d3 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/Icon.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon + +sealed class Icon(open val key: String) { + data class Default(override val key: String) : Icon(key) + + data class Custom( + override val key: String, + val fileResourceUid: String, + val path: String?, + ) : Icon(key) +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt new file mode 100644 index 0000000000..8a353def64 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon + +import io.reactivex.Single +import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyObjectRepository +import org.hisp.dhis.android.core.fileresource.internal.FileResourceStore +import org.hisp.dhis.android.core.icon.internal.CustomIconStore +import org.koin.core.annotation.Singleton + +@Singleton +@Suppress("TooManyFunctions") +class IconCollectionRepository internal constructor( + private val customIconStore: CustomIconStore, + private val fileResourceStore: FileResourceStore, +) { + + fun key(key: String): ReadOnlyObjectRepository { + return object : ReadOnlyObjectRepository { + override fun get(): Single { + return Single.fromCallable { blockingGet() } + } + + override fun blockingGet(): Icon? { + return if (DefaultIcon.all.contains(key)) { + Icon.Default(key) + } else { + customIconStore.selectByKey(key)?.let { customIcon -> + val fileResource = fileResourceStore.selectByUid(customIcon.fileResourceUid()) + Icon.Custom(customIcon.key(), customIcon.fileResourceUid(), fileResource?.path()) + } + } + } + + override fun exists(): Single { + return Single.fromCallable { blockingExists() } + } + + override fun blockingExists(): Boolean { + return blockingGet() != null + } + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconModule.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconModule.kt new file mode 100644 index 0000000000..5ba2ee5b4b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon + +interface IconModule { + fun icons(): IconCollectionRepository +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt index bdc25352ba..26be9e2994 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconType.kt @@ -31,4 +31,4 @@ package org.hisp.dhis.android.core.icon enum class IconType { CUSTOM, DEFAULT, -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt index b14f9defc9..531de650e9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconSeeker.kt @@ -45,6 +45,6 @@ internal class CustomIconSeeker( val allIcons = readSingleColumnResults(query) - return allIcons.filterNot { DefaultIcon.all.contains(it)}.toSet() + return allIcons.filterNot { DefaultIcon.all.contains(it) }.toSet() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt index 050349c473..24137481d8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStore.kt @@ -31,4 +31,6 @@ package org.hisp.dhis.android.core.icon.internal import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectWithoutUidStore import org.hisp.dhis.android.core.icon.CustomIcon -internal interface CustomIconStore : ObjectWithoutUidStore +internal interface CustomIconStore : ObjectWithoutUidStore { + fun selectByKey(key: String): CustomIcon? +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt index 47d123405e..a552ee986d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.icon.internal import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper import org.hisp.dhis.android.core.arch.db.stores.binders.internal.WhereStatementBinder @@ -39,7 +40,7 @@ import org.koin.core.annotation.Singleton @Singleton internal class CustomIconStoreImpl( - databaseAdapter: DatabaseAdapter + databaseAdapter: DatabaseAdapter, ) : CustomIconStore, ObjectWithoutUidStoreImpl( databaseAdapter, @@ -50,6 +51,14 @@ internal class CustomIconStoreImpl( CustomIcon::create, ) { + override fun selectByKey(key: String): CustomIcon? { + val whereClause = WhereClauseBuilder() + .appendKeyStringValue(CustomIconTableInfo.Columns.KEY, key) + .build() + + return selectOneWhere(whereClause) + } + companion object { private val BINDER = StatementBinder { o: CustomIcon, w: StatementWrapper -> w.bind(1, o.key()) diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleImpl.kt new file mode 100644 index 0000000000..a91ff9be56 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.icon.internal + +import org.hisp.dhis.android.core.icon.IconCollectionRepository +import org.hisp.dhis.android.core.icon.IconModule +import org.koin.core.annotation.Singleton + +@Singleton +internal class IconModuleImpl( + private val icons: IconCollectionRepository, +) : IconModule { + override fun icons(): IconCollectionRepository { + return icons + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt index 991dd1d72b..d0237b6ea0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconModuleWiper.kt @@ -42,5 +42,6 @@ internal class IconModuleWiper( } override fun wipeData() { + // No data to wipe } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt index c07ca26651..9046c07c68 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/IconService.kt @@ -28,8 +28,6 @@ package org.hisp.dhis.android.core.icon.internal import org.hisp.dhis.android.core.arch.api.fields.internal.Fields -import org.hisp.dhis.android.core.arch.api.filters.internal.Filter -import org.hisp.dhis.android.core.arch.api.filters.internal.Where import org.hisp.dhis.android.core.arch.api.filters.internal.Which import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.icon.CustomIcon From e028d33dd74091851b856676f983d6c33698bebe Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 15 Feb 2024 11:57:54 +0100 Subject: [PATCH 090/222] [ANDROSDK-1630] Create domainTypes for FileResource download --- .../FileResourceCallRealIntegrationShould.kt | 4 ++-- .../internal/DataValueFileResourcePostCall.kt | 4 ++-- .../core/fileresource/FileResourceDownloadConst.kt | 8 ++++++-- .../core/fileresource/FileResourceDownloader.kt | 4 ++++ .../internal/FileResourceDownloadCall.kt | 9 +++++++-- .../internal/FileResourceDownloadParams.kt | 2 ++ .../core/fileresource/internal/FileResourceHelper.kt | 12 +++++------- .../fileresource/internal/FileResourceModuleImpl.kt | 10 +++++++--- .../OldTrackerImporterFileResourcesPostCall.kt | 4 ++-- .../internal/JobReportFileResourceHandler.kt | 4 ++-- .../fileresource/FileResourceDownloaderShould.kt | 6 +++--- 11 files changed, 42 insertions(+), 25 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceCallRealIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceCallRealIntegrationShould.kt index 8d6b843e06..1a07e1286a 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceCallRealIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceCallRealIntegrationShould.kt @@ -33,7 +33,7 @@ import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.data.server.RealServerMother import org.hisp.dhis.android.core.event.EventCreateProjection -import org.hisp.dhis.android.core.fileresource.FileResourceDomainType +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.FileResourceElementType import java.io.File import java.util.* @@ -122,7 +122,7 @@ class FileResourceCallRealIntegrationShould : BaseRealIntegrationTest() { .byProgramUid("eBAyeGv0exc").limit(5).blockingDownload() d2.fileResourceModule().fileResourceDownloader() - .byDomainType().eq(FileResourceDomainType.TRACKER) + .byDataDomainType().eq(FileResourceDataDomainType.TRACKER) .byElementType().eq(FileResourceElementType.DATA_ELEMENT) .blockingDownload() diff --git a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueFileResourcePostCall.kt b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueFileResourcePostCall.kt index a642b2e675..fbea355287 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueFileResourcePostCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueFileResourcePostCall.kt @@ -28,7 +28,7 @@ package org.hisp.dhis.android.core.datavalue.internal import org.hisp.dhis.android.core.datavalue.DataValue -import org.hisp.dhis.android.core.fileresource.FileResourceDomainType +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.internal.FileResourceHelper import org.hisp.dhis.android.core.fileresource.internal.FileResourcePostCall import org.hisp.dhis.android.core.fileresource.internal.FileResourceValue @@ -62,7 +62,7 @@ internal class DataValueFileResourcePostCall( } fun updateFileResourceStates(fileResources: List) { - fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDomainType.AGGREGATED) + fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDataDomainType.AGGREGATED) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt index 5e15aaaf97..d34f57a01b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt @@ -39,8 +39,12 @@ enum class FileResourceElementType { TRACED_ENTITY_ATTRIBUTE, } -enum class FileResourceDomainType { +enum class FileResourceDataDomainType { AGGREGATED, TRACKER, - CUSTOM_ICON, +} + +enum class FileResourceDomainType(internal val domainType: FileResourceDomain) { + DATA_VALUE(FileResourceDomain.DATA_VALUE), + CUSTOM_ICON(FileResourceDomain.CUSTOM_ICON), } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloader.kt index 7440d0432d..062ce75bbe 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloader.kt @@ -71,6 +71,10 @@ class FileResourceDownloader internal constructor( return connectorFactory.listConnector { list -> params.copy(elementTypes = list) } } + fun byDataDomainType(): ListFilterConnector { + return connectorFactory.listConnector { list -> params.copy(dataDomainTypes = list) } + } + fun byDomainType(): ListFilterConnector { return connectorFactory.listConnector { list -> params.copy(domainTypes = list) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt index 6e87b943c1..d7b0c7d851 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt @@ -39,6 +39,7 @@ import org.hisp.dhis.android.core.arch.helpers.FileResizerHelper import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.fileresource.FileResource +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.FileResourceDomainType import org.hisp.dhis.android.core.fileresource.FileResourceElementType import org.hisp.dhis.android.core.fileresource.FileResourceInternalAccessor @@ -88,7 +89,9 @@ internal class FileResourceDownloadCall( params: FileResourceDownloadParams, existingFileResources: List, ) { - if (params.domainTypes.contains(FileResourceDomainType.AGGREGATED)) { + if (params.domainTypes.contains(FileResourceDomainType.DATA_VALUE) && + params.dataDomainTypes.contains(FileResourceDataDomainType.AGGREGATED) + ) { val dataValues = helper.getMissingAggregatedDataValues(params, existingFileResources) downloadAndPersistFiles( @@ -109,7 +112,9 @@ internal class FileResourceDownloadCall( } private suspend fun downloadTrackerValues(params: FileResourceDownloadParams, existingFileResources: List) { - if (params.domainTypes.contains(FileResourceDomainType.TRACKER)) { + if (params.domainTypes.contains(FileResourceDomainType.DATA_VALUE) && + params.dataDomainTypes.contains(FileResourceDataDomainType.TRACKER) + ) { if (params.elementTypes.contains(FileResourceElementType.TRACED_ENTITY_ATTRIBUTE)) { val attributeDataValues = helper.getMissingTrackerAttributeValues(params, existingFileResources) diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt index 5fcbb2575d..d694f56f67 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadParams.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.fileresource.internal import org.hisp.dhis.android.core.arch.repositories.scope.BaseScope +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.FileResourceDomainType import org.hisp.dhis.android.core.fileresource.FileResourceElementType import org.hisp.dhis.android.core.fileresource.FileResourceValueType @@ -35,6 +36,7 @@ import org.hisp.dhis.android.core.fileresource.FileResourceValueType internal data class FileResourceDownloadParams( val valueTypes: List = FileResourceValueType.entries, val elementTypes: List = FileResourceElementType.entries, + val dataDomainTypes: List = FileResourceDataDomainType.entries, val domainTypes: List = FileResourceDomainType.entries, val maxContentLength: Int? = null, ) : BaseScope diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt index 4a631c38b4..d3a0b530d3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceHelper.kt @@ -36,7 +36,7 @@ import org.hisp.dhis.android.core.datavalue.internal.DataValueStore import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.internal.EventStore import org.hisp.dhis.android.core.fileresource.FileResource -import org.hisp.dhis.android.core.fileresource.FileResourceDomainType +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.trackedentity.* import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStore import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeValueStore @@ -121,7 +121,7 @@ internal class FileResourceHelper( } } - fun updateFileResourceStates(fileResources: List, domainType: FileResourceDomainType) { + fun updateFileResourceStates(fileResources: List, domainType: FileResourceDataDomainType) { fileResources.forEach { fr -> val relatedState = getRelatedResourceState(fr, domainType) val state = if (relatedState == State.SYNCED) State.SYNCED else State.TO_POST @@ -129,17 +129,15 @@ internal class FileResourceHelper( } } - private fun getRelatedResourceState(fileResourceUid: String, domain: FileResourceDomainType): State { + private fun getRelatedResourceState(fileResourceUid: String, domain: FileResourceDataDomainType): State { return when (domain) { - FileResourceDomainType.TRACKER -> + FileResourceDataDomainType.TRACKER -> getRelatedEvent(fileResourceUid)?.syncState() ?: getRelatedTei(fileResourceUid)?.syncState() ?: State.TO_POST - FileResourceDomainType.AGGREGATED -> + FileResourceDataDomainType.AGGREGATED -> getRelatedDataValue(fileResourceUid)?.syncState() ?: State.TO_POST - FileResourceDomainType.CUSTOM_ICON -> - State.SYNCED } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceModuleImpl.kt index a0331c9e20..c5b41c4079 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceModuleImpl.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.fileresource.internal import io.reactivex.Observable import org.hisp.dhis.android.core.arch.call.D2Progress import org.hisp.dhis.android.core.fileresource.FileResourceCollectionRepository +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.FileResourceDomainType import org.hisp.dhis.android.core.fileresource.FileResourceDownloader import org.hisp.dhis.android.core.fileresource.FileResourceModule @@ -46,7 +47,8 @@ internal class FileResourceModuleImpl( "Replace with fileResourceDownloader()", replaceWith = ReplaceWith( expression = "fileResourceDownloader()\n" + - " .byDomainType().eq(FileResourceDomainType.TRACKER)\n" + + " .byDomainType().eq(FileResourceDomainType.DATA_VALUE)\n" + + " .byDataDomainType().eq(FileResourceDataDomainType.TRACKER)\n" + " .byValueType().eq(FileResourceValueType.IMAGE)\n" + " .download()", "org.hisp.dhis.android.core.fileresource.FileResourceDomainType", @@ -55,7 +57,8 @@ internal class FileResourceModuleImpl( ) override fun download(): Observable { return fileResourceDownloader() - .byDomainType().eq(FileResourceDomainType.TRACKER) + .byDomainType().eq(FileResourceDomainType.DATA_VALUE) + .byDataDomainType().eq(FileResourceDataDomainType.TRACKER) .byValueType().eq(FileResourceValueType.IMAGE) .download() } @@ -64,7 +67,8 @@ internal class FileResourceModuleImpl( "Replace with fileResourceDownloader()", replaceWith = ReplaceWith( expression = "fileResourceDownloader()\n" + - " .byDomainType().eq(FileResourceDomainType.TRACKER)\n" + + " .byDomainType().eq(FileResourceDomainType.DATA_VALUE)\n" + + " .byDataDomainType().eq(FileResourceDataDomainType.TRACKER)\n" + " .byValueType().eq(FileResourceValueType.IMAGE)\n" + " .blockingDownload()", "org.hisp.dhis.android.core.fileresource.FileResourceDomainType", diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterFileResourcesPostCall.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterFileResourcesPostCall.kt index 23580db8f9..7196990546 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterFileResourcesPostCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterFileResourcesPostCall.kt @@ -31,7 +31,7 @@ import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentInternalAccessor import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.fileresource.FileResource -import org.hisp.dhis.android.core.fileresource.FileResourceDomainType +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.internal.FileResourceHelper import org.hisp.dhis.android.core.fileresource.internal.FileResourcePostCall import org.hisp.dhis.android.core.fileresource.internal.FileResourceValue @@ -150,7 +150,7 @@ internal class OldTrackerImporterFileResourcesPostCall internal constructor( } fun updateFileResourceStates(fileResources: List) { - fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDomainType.TRACKER) + fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDataDomainType.TRACKER) } @Suppress("TooGenericExceptionCaught") diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportFileResourceHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportFileResourceHandler.kt index df58c91c59..7d7b3802ab 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportFileResourceHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportFileResourceHandler.kt @@ -29,7 +29,7 @@ package org.hisp.dhis.android.core.tracker.importer.internal import org.hisp.dhis.android.core.arch.call.internal.D2ProgressManager import org.hisp.dhis.android.core.fileresource.FileResource -import org.hisp.dhis.android.core.fileresource.FileResourceDomainType +import org.hisp.dhis.android.core.fileresource.FileResourceDataDomainType import org.hisp.dhis.android.core.fileresource.internal.FileResourceHelper import org.koin.core.annotation.Singleton @@ -42,7 +42,7 @@ internal class JobReportFileResourceHandler internal constructor( val fileResources = jobObjects.flatMap { it.fileResources() } - fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDomainType.TRACKER) + fileResourceHelper.updateFileResourceStates(fileResources, FileResourceDataDomainType.TRACKER) progress.increaseProgress(FileResource::class.java, false) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloaderShould.kt b/core/src/test/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloaderShould.kt index fd2f08e080..037649b8e8 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloaderShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloaderShould.kt @@ -55,7 +55,7 @@ class FileResourceDownloaderShould { verify(call).download(paramsCapture.capture()) val params = paramsCapture.firstValue - assertThat(params.domainTypes).isNotEmpty() + assertThat(params.dataDomainTypes).isNotEmpty() assertThat(params.elementTypes).isNotEmpty() assertThat(params.valueTypes).isNotEmpty() assertThat(params.maxContentLength).isNull() @@ -64,7 +64,7 @@ class FileResourceDownloaderShould { @Test fun should_override_default_params() { downloader - .byDomainType().eq(FileResourceDomainType.TRACKER) + .byDataDomainType().eq(FileResourceDataDomainType.TRACKER) .byElementType().eq(FileResourceElementType.DATA_ELEMENT) .byValueType().eq(FileResourceValueType.IMAGE) .byMaxContentLength().eq(400) @@ -73,7 +73,7 @@ class FileResourceDownloaderShould { verify(call).download(paramsCapture.capture()) val params = paramsCapture.firstValue - assertThat(params.domainTypes).isEqualTo(listOf(FileResourceDomainType.TRACKER)) + assertThat(params.dataDomainTypes).isEqualTo(listOf(FileResourceDataDomainType.TRACKER)) assertThat(params.elementTypes).isEqualTo(listOf(FileResourceElementType.DATA_ELEMENT)) assertThat(params.valueTypes).isEqualTo(listOf(FileResourceValueType.IMAGE)) assertThat(params.maxContentLength).isEqualTo(400) From 0ce41d567364b39e9a0e0c469290cd9bbd088334 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 15 Feb 2024 12:52:35 +0100 Subject: [PATCH 091/222] [ANDROSDK-1630] Add test to verify tables with style --- .../internal/TablesWithStyleShould.kt | 95 +++++++++++++++++++ .../internal/TableWithObjectStyle.kt | 1 - .../dhis/android/core/icon/CustomIcon.java | 4 +- 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/common/objectstyle/internal/TablesWithStyleShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/common/objectstyle/internal/TablesWithStyleShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/common/objectstyle/internal/TablesWithStyleShould.kt new file mode 100644 index 0000000000..e794b4ecc9 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/common/objectstyle/internal/TablesWithStyleShould.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.common.objectstyle.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.common.NameableWithStyleColumns +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class TablesWithStyleShould { + + private val databaseAdapter = TestDatabaseAdapterFactory.get() + + private val excludedTables: List = listOf() + + @Test + fun check_content_of_styled_tables() { + val tableListCursor = databaseAdapter.rawQuery("SELECT name FROM sqlite_master WHERE type='table'") + val tableList = readStringColumn(tableListCursor, "name") + + val tablesWithStyle = tableList + .filterNot { excludedTables.contains(it) } + .filter { table -> + val columnListCursor = databaseAdapter.rawQuery("PRAGMA table_info($table);") + val columns = readStringColumn(columnListCursor, "name") + + columns.contains(NameableWithStyleColumns.ICON) + } + + val missingTablesInList = tablesWithStyle.minus(TableWithObjectStyle.allTableNames.toSet()) + val exceedingTablesInList = TableWithObjectStyle.allTableNames.minus(tablesWithStyle.toSet()) + + if (missingTablesInList.isNotEmpty() || exceedingTablesInList.isNotEmpty()) { + missingTablesInList.forEach { + println( + "Table $it is not in TableWithStyle list. " + + "Add it to the list or to the excluded tables in the test", + ) + } + + exceedingTablesInList.forEach { + println( + "Table $it is in the TableWithStyle list but has no style column. " + + "Remove it from the list.", + ) + } + + fail("Tables with style don't match with tables in TableWithStyle list.") + } + } + + private fun readStringColumn(cursor: Cursor, column: String): List { + val result = mutableListOf() + cursor.use { c -> + if (c.count > 0) { + c.moveToFirst() + do { + val tableIdx = c.getColumnIndex(column) + val tableName = c.getString(tableIdx) + result.add(tableName) + } while (c.moveToNext()) + } + } + return result + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt index 535561ae65..996542df8c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/common/objectstyle/internal/TableWithObjectStyle.kt @@ -40,7 +40,6 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceFilterTable import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeTableInfo internal object TableWithObjectStyle { - // TODO Test table containing icon val allTableNames: List = setOf( DataElementTableInfo.TABLE_INFO, DataSetTableInfo.TABLE_INFO, diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java index ed695791e2..4e0b76a409 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java @@ -40,7 +40,7 @@ import org.hisp.dhis.android.core.common.CoreObject; @AutoValue -@JsonDeserialize(builder = AutoValue_CustomIcon.Builder.class) +@JsonDeserialize(builder = $$AutoValue_CustomIcon.Builder.class) public abstract class CustomIcon implements CoreObject { @NonNull @@ -57,7 +57,7 @@ public abstract class CustomIcon implements CoreObject { @NonNull public static CustomIcon create(Cursor cursor) { - return AutoValue_CustomIcon.createFromCursor(cursor); + return $AutoValue_CustomIcon.createFromCursor(cursor); } public abstract Builder toBuilder(); From 7a2520ba776873dd2706675aac21e5ab77c30d04 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 16 Feb 2024 11:56:08 +0100 Subject: [PATCH 092/222] [ANDROSDK-1811] Adapt AnalyticsDhisVisualization to TrackerVisualizations --- ...ngObjectRepositoryMockIntegrationShould.kt | 2 +- ...ObjectRepositoryMockIntegrationShould.java | 2 +- core/src/main/assets/migrations/161.sql | 6 + core/src/main/assets/snapshots/snapshot.sql | 2 +- .../access/internal/BaseDatabaseOpenHelper.kt | 2 +- ...icsDhisVisualizationScopeColumnAdapter.kt} | 12 +- ...yticsDhisVisualizationTypeColumnAdapter.kt | 36 +++++ .../core/mockwebserver/Dhis2MockServer.java | 2 +- .../settings/AnalyticsDhisVisualization.java | 25 +++- .../AnalyticsDhisVisualizationTableInfo.kt | 2 + .../AnalyticsDhisVisualizationType.kt | 34 +++++ .../AnalyticsDhisVisualizationStoreImpl.kt | 1 + .../TrackerVisualizationModuleDownloader.kt | 11 +- .../internal/VisualizationModuleDownloader.kt | 6 +- .../data/settings/AnalyticsSettingsSamples.kt | 2 + .../settings/analytics_settings_v2.json | 4 - .../settings/analytics_settings_v3.json | 131 ++++++++++++++++++ .../core/settings/AnalyticsSettingAsserts.kt | 131 ++++++++++++++++++ .../core/settings/AnalyticsSettingV1Should.kt | 61 +------- .../core/settings/AnalyticsSettingV2Should.kt | 62 +-------- .../core/settings/AnalyticsSettingV3Should.kt | 64 +++++++++ 21 files changed, 459 insertions(+), 139 deletions(-) create mode 100644 core/src/main/assets/migrations/161.sql rename core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/{AnalyticsDhisVisualizationScopeColumnAdapter.java => AnalyticsDhisVisualizationScopeColumnAdapter.kt} (86%) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationTypeColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationType.kt create mode 100644 core/src/sharedTest/resources/settings/analytics_settings_v3.json create mode 100644 core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV3Should.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt index 3dc0d1845a..dd183c732a 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt @@ -60,7 +60,7 @@ class AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould : .blockingGetByProgram("IpHINAT79UW") assertThat(programSettings?.size).isEqualTo(1) - assertThat(programSettings?.first()?.visualizations()?.size).isEqualTo(2) + assertThat(programSettings?.first()?.visualizations()?.size).isEqualTo(1) assertThat( programSettings?.first()?.visualizations()?.first()?.uid(), ).isEqualTo("PYBH8ZaAQnC") diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsSettingsObjectRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsSettingsObjectRepositoryMockIntegrationShould.java index 80e13b6cc1..47e918b937 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsSettingsObjectRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsSettingsObjectRepositoryMockIntegrationShould.java @@ -68,7 +68,7 @@ public void find_analytics_settings() { for (AnalyticsDhisVisualizationsGroup analyticsDhisVisualizationsGroup : analyticsSettings.dhisVisualizations().home()) { if (analyticsDhisVisualizationsGroup.id().equals("12345678910")) { - assertThat(analyticsDhisVisualizationsGroup.visualizations().size()).isEqualTo(2); + assertThat(analyticsDhisVisualizationsGroup.visualizations().size()).isEqualTo(3); } else if (analyticsDhisVisualizationsGroup.id().equals("12345678911")) { assertThat(analyticsDhisVisualizationsGroup.visualizations().size()).isEqualTo(1); } diff --git a/core/src/main/assets/migrations/161.sql b/core/src/main/assets/migrations/161.sql new file mode 100644 index 0000000000..d6d6ec623e --- /dev/null +++ b/core/src/main/assets/migrations/161.sql @@ -0,0 +1,6 @@ +# Add TrackerVisualization to ASWA (ANDROSDK-1811) + +ALTER TABLE AnalyticsDhisVisualization RENAME TO AnalyticsDhisVisualization_Old; +CREATE TABLE AnalyticsDhisVisualization (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, scopeUid TEXT, scope TEXT, groupUid TEXT, groupName TEXT, timestamp TEXT, name TEXT, type TEXT NOT NULL); +INSERT INTO AnalyticsDhisVisualization(_id, uid, scopeUid, scope, groupUid, groupName, timestamp, name, type) SELECT _id, uid, scopeUid, scope, groupUid, groupName, timestamp, name, 'VISUALIZATION' FROM AnalyticsDhisVisualization_Old; +DROP TABLE IF EXISTS AnalyticsDhisVisualization_Old; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 4d0421c938..df3124a9bb 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -109,7 +109,7 @@ CREATE TABLE DataElementAttributeValueLink (_id INTEGER PRIMARY KEY AUTOINCREMEN CREATE TABLE ProgramAttributeValueLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, program TEXT NOT NULL, attribute TEXT NOT NULL, value TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (attribute) REFERENCES Attribute (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (program, attribute)); CREATE TABLE TrackerJobObject (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerType TEXT NOT NULL, objectUid TEXT NOT NULL, jobUid TEXT NOT NULL, lastUpdated TEXT NOT NULL, fileResources TEXT); CREATE TABLE DataValueConflict (_id INTEGER PRIMARY KEY AUTOINCREMENT, conflict TEXT, value TEXT, attributeOptionCombo TEXT, categoryOptionCombo TEXT, dataElement TEXT, period TEXT, orgUnit TEXT, errorCode TEXT, status TEXT, created TEXT, displayDescription TEXT); -CREATE TABLE AnalyticsDhisVisualization (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, scopeUid TEXT, scope TEXT, groupUid TEXT, groupName TEXT, timestamp TEXT, name TEXT, FOREIGN KEY (uid) REFERENCES Visualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE AnalyticsDhisVisualization (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, scopeUid TEXT, scope TEXT, groupUid TEXT, groupName TEXT, timestamp TEXT, name TEXT, type TEXT NOT NULL); CREATE TABLE Visualization (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, displayFormName TEXT, title TEXT, displayTitle TEXT, subtitle TEXT, displaySubtitle TEXT, type TEXT, hideTitle INTEGER, hideSubtitle INTEGER, hideEmptyColumns INTEGER, hideEmptyRows INTEGER, hideEmptyRowItems TEXT, hideLegend INTEGER, showHierarchy INTEGER, rowTotals INTEGER, rowSubTotals INTEGER, colTotals INTEGER, colSubTotals INTEGER, showDimensionLabels INTEGER, percentStackedValues INTEGER, noSpaceBetweenColumns INTEGER, skipRounding INTEGER, displayDensity TEXT, digitGroupSeparator TEXT, legendShowKey TEXT, legendStyle TEXT, legendSetId TEXT, legendStrategy TEXT, aggregationType TEXT); CREATE TABLE VisualizationDimensionItem(_id INTEGER PRIMARY KEY AUTOINCREMENT, visualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionItem TEXT, dimensionItemType TEXT, FOREIGN KEY (visualization) REFERENCES Visualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE LocalDataStore (_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL UNIQUE, value TEXT); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index 31ca52da58..e8a584ea5a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 160 + const val VERSION = 161 } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.kt similarity index 86% rename from core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.java rename to core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.kt index 4c5afef0fa..0573c292b8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationScopeColumnAdapter.kt @@ -25,14 +25,12 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal -package org.hisp.dhis.android.core.arch.db.adapters.enums.internal; +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationScope -import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationScope; - -public class AnalyticsDhisVisualizationScopeColumnAdapter extends EnumColumnAdapter { - @Override - protected Class getEnumClass() { - return AnalyticsDhisVisualizationScope.class; +internal class AnalyticsDhisVisualizationScopeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return AnalyticsDhisVisualizationScope::class.java } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationTypeColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationTypeColumnAdapter.kt new file mode 100644 index 0000000000..56ae10991f --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/AnalyticsDhisVisualizationTypeColumnAdapter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.enums.internal + +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType + +internal class AnalyticsDhisVisualizationTypeColumnAdapter : EnumColumnAdapter() { + override fun getEnumClass(): Class { + return AnalyticsDhisVisualizationType::class.java + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index d740834080..f540c92aa6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -63,7 +63,7 @@ public class Dhis2MockServer { private static final String PROGRAM_SETTINGS_JSON = "settings/program_settings.json"; private static final String SYNCHRONIZATION_SETTTINGS_JSON = "settings/synchronization_settings.json"; private static final String APPEARANCE_SETTINGS_JSON = "settings/appearance_settings_v2.json"; - private static final String ANALYTICS_SETTINGS_JSON = "settings/analytics_settings_v2.json"; + private static final String ANALYTICS_SETTINGS_JSON = "settings/analytics_settings_v3.json"; private static final String USER_SETTINGS_JSON = "settings/user_settings.json"; private static final String LATEST_APP_VERSION_JSON = "settings/latest_app_version.json"; private static final String PROGRAMS_JSON = "program/programs.json"; diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java index b4516ea181..2ebedb70d3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java @@ -40,7 +40,9 @@ import com.google.auto.value.AutoValue; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.AnalyticsDhisVisualizationScopeColumnAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.AnalyticsDhisVisualizationTypeColumnAdapter; import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectStyle; import org.hisp.dhis.android.core.common.ObjectWithUidInterface; import static org.hisp.dhis.android.core.common.BaseIdentifiableObject.UID; @@ -67,11 +69,18 @@ public abstract class AnalyticsDhisVisualization implements CoreObject, ObjectWi public abstract String uid(); @Nullable + @JsonProperty public abstract String name(); @Nullable + @JsonProperty public abstract String timestamp(); + @NonNull + @JsonProperty + @ColumnAdapter(AnalyticsDhisVisualizationTypeColumnAdapter.class) + public abstract AnalyticsDhisVisualizationType type(); + public static AnalyticsDhisVisualization create(Cursor cursor) { return AutoValue_AnalyticsDhisVisualization.createFromCursor(cursor); } @@ -101,9 +110,21 @@ public abstract static class Builder { public abstract Builder name(String name); - @JsonProperty("timestamp") public abstract Builder timestamp(String timestamp); - public abstract AnalyticsDhisVisualization build(); + public abstract Builder type(AnalyticsDhisVisualizationType type); + + abstract AnalyticsDhisVisualization autoBuild(); + + abstract AnalyticsDhisVisualizationType type(); + + public AnalyticsDhisVisualization build() { + try { + type(); + } catch (IllegalStateException e) { + type(AnalyticsDhisVisualizationType.VISUALIZATION); + } + return autoBuild(); + } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationTableInfo.kt index bdf6151182..1710b3b9b8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationTableInfo.kt @@ -57,6 +57,7 @@ object AnalyticsDhisVisualizationTableInfo { GROUP_NAME, NAME, TIME_STAMP, + TYPE, ) } @@ -68,6 +69,7 @@ object AnalyticsDhisVisualizationTableInfo { const val GROUP_NAME = "groupName" const val NAME = "name" const val TIME_STAMP = "timestamp" + const val TYPE = "type" } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationType.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationType.kt new file mode 100644 index 0000000000..a0025b1422 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualizationType.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings + +enum class AnalyticsDhisVisualizationType { + VISUALIZATION, + TRACKER_VISUALIZATION, +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationStoreImpl.kt index 46b703c7aa..8b645ef2b0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationStoreImpl.kt @@ -61,6 +61,7 @@ internal class AnalyticsDhisVisualizationStoreImpl( w.bind(5, o.groupName()) w.bind(6, o.name()) w.bind(7, o.timestamp()) + w.bind(8, o.type()) } private val WHERE_UPDATE_BINDER = WhereStatementBinder { _: AnalyticsDhisVisualization, _: StatementWrapper -> diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt index 5fb6459119..6756855760 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationModuleDownloader.kt @@ -28,18 +28,23 @@ package org.hisp.dhis.android.core.visualization.internal import org.hisp.dhis.android.core.arch.modules.internal.TypedModuleDownloader +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType +import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationStore import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.koin.core.annotation.Singleton @Singleton internal class TrackerVisualizationModuleDownloader internal constructor( private val visualizationCall: TrackerVisualizationCall, + private val analyticsDhisVisualizationStore: AnalyticsDhisVisualizationStore, ) : TypedModuleDownloader> { override suspend fun downloadMetadata(): List { - // Extract visualizations in ANDROSDK-1811 - val trackerVisualizations = setOf("s85urBIkN0z") - return visualizationCall.download(trackerVisualizations) + val visualizations = analyticsDhisVisualizationStore.selectAll() + .filter { it.type() == AnalyticsDhisVisualizationType.TRACKER_VISUALIZATION } + .map { it.uid() }.toSet() + + return visualizationCall.download(visualizations) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleDownloader.kt index 5c0a22049c..256e6e6572 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationModuleDownloader.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.visualization.internal import org.hisp.dhis.android.core.arch.modules.internal.TypedModuleDownloader +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationStore import org.hisp.dhis.android.core.visualization.Visualization import org.koin.core.annotation.Singleton @@ -40,7 +41,10 @@ internal class VisualizationModuleDownloader internal constructor( TypedModuleDownloader> { override suspend fun downloadMetadata(): List { - val visualizations = analyticsDhisVisualizationStore.selectAll().map { it.uid() }.toSet() + val visualizations = analyticsDhisVisualizationStore.selectAll() + .filter { it.type() == AnalyticsDhisVisualizationType.VISUALIZATION } + .map { it.uid() }.toSet() + return visualizationCall.download(visualizations) } } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/AnalyticsSettingsSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/AnalyticsSettingsSamples.kt index 50f5644d3b..19114e4c0b 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/AnalyticsSettingsSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/AnalyticsSettingsSamples.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.data.settings import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualization import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationScope +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType import org.hisp.dhis.android.core.settings.AnalyticsTeiAttribute import org.hisp.dhis.android.core.settings.AnalyticsTeiData import org.hisp.dhis.android.core.settings.AnalyticsTeiDataElement @@ -112,5 +113,6 @@ object AnalyticsSettingsSamples { .timestamp("2021-07-01T02:55:16.8770") .uid("PYBH8ZaAQnC") .name("Sample name") + .type(AnalyticsDhisVisualizationType.VISUALIZATION) .build() } diff --git a/core/src/sharedTest/resources/settings/analytics_settings_v2.json b/core/src/sharedTest/resources/settings/analytics_settings_v2.json index bb7b0941d1..93cf13e54c 100644 --- a/core/src/sharedTest/resources/settings/analytics_settings_v2.json +++ b/core/src/sharedTest/resources/settings/analytics_settings_v2.json @@ -109,10 +109,6 @@ "id": "0000000001", "name": "default", "visualizations": [ - { - "id": "PYBH8ZaAQnC", - "timestamp": "2021-07-01T02:55:16.8770" - }, { "id": "PYBH8ZaAQnC", "timestamp": "2021-07-01T02:55:16.8770" diff --git a/core/src/sharedTest/resources/settings/analytics_settings_v3.json b/core/src/sharedTest/resources/settings/analytics_settings_v3.json new file mode 100644 index 0000000000..b5bb0d22aa --- /dev/null +++ b/core/src/sharedTest/resources/settings/analytics_settings_v3.json @@ -0,0 +1,131 @@ +{ + "tei": [ + { + "uid": "fqEx2avRp1L", + "data": { + "dataElements": [ + "dBwrot7S420.sWoqcoByYmD", + "dBwrot7S421.Ok9OQpitjQr" + ] + }, + "name": "Height evolution", + "type": "LINE", + "period": "Monthly", + "program": "IpHINAT79UW", + "programStage": "dBwrot7S420", + "shortName": "H. evolution" + }, + { + "uid": "XQUhloISaQJ", + "data": { + "programIndicators": [ + "dBwrot7S420.GSae40Fyppf" + ], + "attributes": [ + "cejWyOfXge6" + ] + }, + "name": "Weight gain", + "type": "BAR", + "period": "Weekly", + "program": "lxAQ7Zs9VYR", + "shortName": "W. gain" + }, + { + "WHONutrition": { + "chartType": "WFH", + "gender": { + "attribute": "cejWyOfXge6", + "values": { + "female": "female", + "male": "male" + } + }, + "x": { + "dataElements": [ + "dBwrot7S420.sWoqcoByYmD" + ] + }, + "y": { + "programIndicators": [ + "GSae40Fyppf" + ] + } + }, + "name": "Who chart", + "program": "IpHINAT79UW", + "programStage": "dBwrot7S420", + "shortName": "Who chart", + "type": "WHO_NUTRITION", + "uid": "yEdtdG7ql9K" + } + ], + "lastUpdated": "2021-06-02T04:30:16.877Z", + "dhisVisualizations": { + "home": [ + { + "id": "12345678910", + "name": "Ejemplo", + "visualizations": [ + { + "id": "FAFa11yFeFe", + "name": "Sample title fro visualization", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T03:01:16.8770" + }, + { + "id": "PYBH8ZaAQnC", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T03:02:16.8770" + }, + { + "id": "s85urBIkN0z", + "type": "TRACKER_VISUALIZATION", + "timestamp": "2021-07-01T03:02:16.8770" + } + ] + }, + { + "id": "12345678911", + "name": "Otro ejemplo", + "visualizations": [ + { + "id": "PYBH8ZaAQnC", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T03:04:16.8770" + } + ] + } + ], + "dataSet": { + "BfMAe6Itzgt": [ + { + "id": "0000000001", + "name": "default", + "visualizations": [ + { + "id": "FAFa11yFeFe", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T02:55:16.8770" + } + ] + } + ] + }, + "program": { + "IpHINAT79UW": [ + { + "id": "0000000001", + "name": "default", + "visualizations": [ + { + "id": "PYBH8ZaAQnC", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T02:55:16.8770" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt new file mode 100644 index 0000000000..c4ea8abd85 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.period.PeriodType +import org.junit.Assert.fail + +object AnalyticsSettingAsserts { + fun assertTeiAnalytics(teis: List) { + assertThat(teis.size).isEqualTo(3) + + teis.forEach { tei -> + when (tei.uid()) { + "fqEx2avRp1L" -> { + assertThat(tei.name()).isEqualTo("Height evolution") + assertThat(tei.shortName()).isEqualTo("H. evolution") + assertThat(tei.program()).isEqualTo("IpHINAT79UW") + assertThat(tei.programStage()).isEqualTo("dBwrot7S420") + assertThat(tei.period()).isEquivalentAccordingToCompareTo(PeriodType.Monthly) + assertThat(tei.type()).isEquivalentAccordingToCompareTo(ChartType.LINE) + assertThat(tei.data()?.dataElements()?.size).isEqualTo(2) + + assertThat( + tei.data()?.dataElements()?.any { dataElement -> + dataElement.dataElement() == "sWoqcoByYmD" && dataElement.programStage() == "dBwrot7S420" + }, + ).isTrue() + + assertThat( + tei.data()?.dataElements()?.any { dataElement -> + dataElement.dataElement() == "Ok9OQpitjQr" && dataElement.programStage() == "dBwrot7S421" + }, + ).isTrue() + } + "XQUhloISaQJ" -> { + assertThat(tei.name()).isEqualTo("Weight gain") + + assertThat(tei.data()?.indicators()?.size).isEqualTo(1) + assertThat( + tei.data()?.indicators()?.first()?.let { + it.indicator() == "GSae40Fyppf" && it.programStage() == "dBwrot7S420" + }, + ).isTrue() + + assertThat(tei.data()?.attributes()?.size).isEqualTo(1) + assertThat(tei.data()?.attributes()?.first()?.attribute() == "cejWyOfXge6").isTrue() + } + "yEdtdG7ql9K" -> { + assertThat(tei.name()).isEqualTo("Who chart") + + assertThat(tei.whoNutritionData()).isNotNull() + assertThat(tei.whoNutritionData()?.chartType()) + .isEquivalentAccordingToCompareTo(WHONutritionChartType.WFH) + + assertThat(tei.whoNutritionData()?.gender()?.attribute()).isEqualTo("cejWyOfXge6") + assertThat(tei.whoNutritionData()?.gender()?.values()?.male()).isEqualTo("male") + assertThat(tei.whoNutritionData()?.gender()?.values()?.female()).isEqualTo("female") + + assertThat(tei.whoNutritionData()?.x()?.dataElements()?.size).isEqualTo(1) + assertThat(tei.whoNutritionData()?.x()?.indicators()?.size).isEqualTo(0) + assertThat(tei.whoNutritionData()?.y()?.dataElements()?.size).isEqualTo(0) + assertThat(tei.whoNutritionData()?.y()?.indicators()?.size).isEqualTo(1) + } + else -> fail("Unexpected tei uid") + } + } + } + + fun assertDhisVisualizations(dhisVisualizationSetting: AnalyticsDhisVisualizationsSetting) { + assertThat(dhisVisualizationSetting.home().size).isEqualTo(2) + dhisVisualizationSetting.home().forEach { group -> + when (group.id()) { + "12345678910" -> { + assertThat(group.name()).isEqualTo("Ejemplo") + assertThat(group.visualizations().size).isEqualTo(2) + } + "12345678911" -> { + assertThat(group.name()).isEqualTo("Otro ejemplo") + assertThat(group.visualizations().size).isEqualTo(1) + } + } + } + + assertThat(dhisVisualizationSetting.dataSet().size).isEqualTo(1) + dhisVisualizationSetting.dataSet().forEach { map -> + when (map.key) { + "BfMAe6Itzgt" -> { + assertThat(map.value.size).isEqualTo(1) + assertThat(map.value[0].visualizations().size).isEqualTo(1) + } + } + } + + assertThat(dhisVisualizationSetting.program().size).isEqualTo(1) + dhisVisualizationSetting.program().forEach { map -> + when (map.key) { + "IpHINAT79UW" -> { + assertThat(map.value.size).isEqualTo(1) + assertThat(map.value[0].visualizations().size).isEqualTo(1) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV1Should.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV1Should.kt index e0ed4fb101..57e255f85e 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV1Should.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV1Should.kt @@ -27,11 +27,8 @@ */ package org.hisp.dhis.android.core.settings -import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.common.BaseObjectShould import org.hisp.dhis.android.core.common.ObjectShould -import org.hisp.dhis.android.core.period.PeriodType -import org.junit.Assert.fail import org.junit.Test import java.io.IOException import java.text.ParseException @@ -43,62 +40,6 @@ class AnalyticsSettingV1Should : BaseObjectShould("settings/analytics_settings.j override fun map_from_json_string() { val analyticsSettings = objectMapper.readValue(jsonStream, AnalyticsSettings::class.java) - assertThat(analyticsSettings.tei().size).isEqualTo(3) - - analyticsSettings.tei().forEach { tei -> - when (tei.uid()) { - "fqEx2avRp1L" -> { - assertThat(tei.name()).isEqualTo("Height evolution") - assertThat(tei.shortName()).isEqualTo("H. evolution") - assertThat(tei.program()).isEqualTo("IpHINAT79UW") - assertThat(tei.programStage()).isEqualTo("dBwrot7S420") - assertThat(tei.period()).isEquivalentAccordingToCompareTo(PeriodType.Monthly) - assertThat(tei.type()).isEquivalentAccordingToCompareTo(ChartType.LINE) - assertThat(tei.data()?.dataElements()?.size).isEqualTo(2) - - assertThat( - tei.data()?.dataElements()?.any { dataElement -> - dataElement.dataElement() == "sWoqcoByYmD" && dataElement.programStage() == "dBwrot7S420" - }, - ).isTrue() - - assertThat( - tei.data()?.dataElements()?.any { dataElement -> - dataElement.dataElement() == "Ok9OQpitjQr" && dataElement.programStage() == "dBwrot7S421" - }, - ).isTrue() - } - "XQUhloISaQJ" -> { - assertThat(tei.name()).isEqualTo("Weight gain") - - assertThat(tei.data()?.indicators()?.size).isEqualTo(1) - assertThat( - tei.data()?.indicators()?.first()?.let { - it.indicator() == "GSae40Fyppf" && it.programStage() == "dBwrot7S420" - }, - ).isTrue() - - assertThat(tei.data()?.attributes()?.size).isEqualTo(1) - assertThat(tei.data()?.attributes()?.first()?.attribute() == "cejWyOfXge6").isTrue() - } - "yEdtdG7ql9K" -> { - assertThat(tei.name()).isEqualTo("Who chart") - - assertThat(tei.whoNutritionData()).isNotNull() - assertThat(tei.whoNutritionData()?.chartType()) - .isEquivalentAccordingToCompareTo(WHONutritionChartType.WFH) - - assertThat(tei.whoNutritionData()?.gender()?.attribute()).isEqualTo("cejWyOfXge6") - assertThat(tei.whoNutritionData()?.gender()?.values()?.male()).isEqualTo("male") - assertThat(tei.whoNutritionData()?.gender()?.values()?.female()).isEqualTo("female") - - assertThat(tei.whoNutritionData()?.x()?.dataElements()?.size).isEqualTo(1) - assertThat(tei.whoNutritionData()?.x()?.indicators()?.size).isEqualTo(0) - assertThat(tei.whoNutritionData()?.y()?.dataElements()?.size).isEqualTo(0) - assertThat(tei.whoNutritionData()?.y()?.indicators()?.size).isEqualTo(1) - } - else -> fail("Unexpected tei uid") - } - } + AnalyticsSettingAsserts.assertTeiAnalytics(analyticsSettings.tei()) } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV2Should.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV2Should.kt index 6556258f95..ab6f7a6f7f 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV2Should.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV2Should.kt @@ -31,73 +31,21 @@ package org.hisp.dhis.android.core.settings import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.common.BaseObjectShould import org.hisp.dhis.android.core.common.ObjectShould -import org.hisp.dhis.android.core.period.PeriodType import org.junit.Test -import java.io.IOException -import java.text.ParseException class AnalyticsSettingV2Should : BaseObjectShould("settings/analytics_settings_v2.json"), ObjectShould { @Test - @Throws(IOException::class, ParseException::class) override fun map_from_json_string() { val analyticsSettings = objectMapper.readValue(jsonStream, AnalyticsSettings::class.java) - assertThat(analyticsSettings.tei().size).isEqualTo(3) + AnalyticsSettingAsserts.assertTeiAnalytics(analyticsSettings.tei()) - analyticsSettings.tei().forEach { tei -> - when (tei.uid()) { - "fqEx2avRp1L" -> { - assertThat(tei.name()).isEqualTo("Height evolution") - assertThat(tei.shortName()).isEqualTo("H. evolution") - assertThat(tei.program()).isEqualTo("IpHINAT79UW") - assertThat(tei.programStage()).isEqualTo("dBwrot7S420") - assertThat(tei.period()).isEquivalentAccordingToCompareTo(PeriodType.Monthly) - assertThat(tei.type()).isEquivalentAccordingToCompareTo(ChartType.LINE) - } - "XQUhloISaQJ" -> { - assertThat(tei.data()?.indicators()?.size).isEqualTo(1) - assertThat(tei.data()?.indicators()?.first()?.programStage()).isEqualTo("dBwrot7S420") - assertThat(tei.data()?.indicators()?.first()?.indicator()).isEqualTo("GSae40Fyppf") - } - "yEdtdG7ql9K" -> { - assertThat(tei.whoNutritionData()?.y()?.indicators()?.size).isEqualTo(1) - assertThat(tei.whoNutritionData()?.y()?.indicators()?.first()?.indicator()).isEqualTo("GSae40Fyppf") - } - } - } - - assertThat(analyticsSettings.dhisVisualizations().home().size).isEqualTo(2) - analyticsSettings.dhisVisualizations().home().forEach { group -> - when (group.id()) { - "12345678910" -> { - assertThat(group.name()).isEqualTo("Ejemplo") - assertThat(group.visualizations().size).isEqualTo(2) - } - "12345678911" -> { - assertThat(group.name()).isEqualTo("Otro ejemplo") - assertThat(group.visualizations().size).isEqualTo(1) - } - } - } - - assertThat(analyticsSettings.dhisVisualizations().dataSet().size).isEqualTo(1) - analyticsSettings.dhisVisualizations().dataSet().forEach { map -> - when (map.key) { - "BfMAe6Itzgt" -> { - assertThat(map.value.size).isEqualTo(1) - assertThat(map.value[0].visualizations().size).isEqualTo(1) - } - } - } + AnalyticsSettingAsserts.assertDhisVisualizations(analyticsSettings.dhisVisualizations()) - assertThat(analyticsSettings.dhisVisualizations().program().size).isEqualTo(1) - analyticsSettings.dhisVisualizations().program().forEach { map -> - when (map.key) { - "IpHINAT79UW" -> { - assertThat(map.value.size).isEqualTo(1) - assertThat(map.value[0].visualizations().size).isEqualTo(2) - } + analyticsSettings.dhisVisualizations().home().forEach { + it.visualizations().forEach { + assertThat(it.type()).isEqualTo(AnalyticsDhisVisualizationType.VISUALIZATION) } } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV3Should.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV3Should.kt new file mode 100644 index 0000000000..a1ae6f5674 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingV3Should.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test +import java.io.IOException +import java.text.ParseException + +class AnalyticsSettingV3Should : BaseObjectShould("settings/analytics_settings_v3.json"), ObjectShould { + + @Test + @Throws(IOException::class, ParseException::class) + override fun map_from_json_string() { + val analyticsSettings = objectMapper.readValue(jsonStream, AnalyticsSettings::class.java) + + AnalyticsSettingAsserts.assertTeiAnalytics(analyticsSettings.tei()) + + AnalyticsSettingAsserts.assertDhisVisualizations(analyticsSettings.dhisVisualizations()) + + analyticsSettings.dhisVisualizations().home().forEach { + it.visualizations().forEach { + when (it.uid()) { + "FAFa11yFeFe" -> + assertThat(it.type()).isEqualTo(AnalyticsDhisVisualizationType.VISUALIZATION) + + "PYBH8ZaAQnC" -> + assertThat(it.type()).isEqualTo(AnalyticsDhisVisualizationType.VISUALIZATION) + + "s85urBIkN0z" -> + assertThat(it.type()).isEqualTo(AnalyticsDhisVisualizationType.TRACKER_VISUALIZATION) + } + } + } + } +} From d6b3bf72314384853d9a3c9f81ce37641b644d45 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 16 Feb 2024 16:55:05 +0100 Subject: [PATCH 093/222] [ANDROSDK-1811] Parse TrackerVisualization in LineList service --- .../core/analytics/AnalyticsDIModule.kt | 2 +- ...icsVisualizationsServiceDimensionHelper.kt | 6 +- .../core/analytics/internal/AnalyticsRegex.kt | 37 +++ .../trackerlinelist/TrackerLineListModel.kt | 27 +- .../TrackerLineListRepository.kt | 3 +- .../internal/TrackerLineListParams.kt | 20 +- .../internal/TrackerLineListRepositoryImpl.kt | 15 +- .../internal/TrackerLineListService.kt | 78 ++++-- .../internal/TrackerVisualizationMapper.kt | 233 ++++++++++++++++++ .../internal/AnalyticsRegexShould.kt | 67 +++++ 10 files changed, 435 insertions(+), 53 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegexShould.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt index 2b35eb85c3..d9998cdcec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsDIModule.kt @@ -56,6 +56,6 @@ internal class AnalyticsDIModule { @Singleton fun emptyTrackerLineListParams(): TrackerLineListParams { - return TrackerLineListParams(null, null, null, emptyList(), emptyList()) + return TrackerLineListParams(null, null, null, null, emptyList(), emptyList()) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/internal/AnalyticsVisualizationsServiceDimensionHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/internal/AnalyticsVisualizationsServiceDimensionHelper.kt index 6037834850..d0732c90fc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/internal/AnalyticsVisualizationsServiceDimensionHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/internal/AnalyticsVisualizationsServiceDimensionHelper.kt @@ -32,6 +32,9 @@ import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.Dimension import org.hisp.dhis.android.core.analytics.aggregated.DimensionItem import org.hisp.dhis.android.core.analytics.aggregated.GridDimension +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.composedUidOperandRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.orgunitLevelRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.uidRegex import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder import org.hisp.dhis.android.core.category.internal.CategoryCategoryOptionLinkStore import org.hisp.dhis.android.core.category.internal.CategoryStore @@ -54,9 +57,6 @@ internal class AnalyticsVisualizationsServiceDimensionHelper( private val dataDimension = "dx" private val orgUnitDimension = "ou" private val periodDimension = "pe" - private val uidRegex = "^\\w{11}\$".toRegex() - private val composedUidOperandRegex = "^(\\w{11})\\.(\\w{11})\$".toRegex() - private val orgunitLevelRegex = "LEVEL-(\\d+)".toRegex() fun getDimensionItems(dimensions: List?): List { return dimensions?.map { dimension -> diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt new file mode 100644 index 0000000000..b382a9b7df --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.internal + +internal object AnalyticsRegex { + val uidRegex = "^\\w{11}\$".toRegex() + val composedUidOperandRegex = "^(\\w{11})\\.(\\w{11})\$".toRegex() + val orgunitLevelRegex = "^LEVEL-(\\d+)\$".toRegex() + val orgunitGroupRegex = "^OU_GROUP-(\\w{11})\$".toRegex() + val dateRangeRegex = "^(\\d{4}-\\d{1,2}-\\d{1,2})_(\\d{4}-\\d{1,2}-\\d{1,2})\$".toRegex() +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 8a6bb38782..1cc1237f83 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -31,15 +31,12 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist import org.hisp.dhis.android.core.common.RelativeOrganisationUnit import org.hisp.dhis.android.core.common.RelativePeriod import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.EventStatus sealed class TrackerLineListItem(val id: String) { - sealed class OrganisationUnitItem(id: String) : TrackerLineListItem(id) { - data class Absolute(val uid: String) : OrganisationUnitItem(uid) - data class Relative(val relative: RelativeOrganisationUnit) : OrganisationUnitItem(relative.name) - data class Level(val uid: String) : OrganisationUnitItem(uid) - data class Group(val uid: String) : OrganisationUnitItem(uid) - } + class OrganisationUnitItem(val filters: List) : + TrackerLineListItem(Label.OrganisationUnit) sealed class DateItem(id: String) : TrackerLineListItem(id) { data class LastUpdated(val filters: List) : DateItem(Label.LastUpdated) @@ -55,18 +52,25 @@ sealed class TrackerLineListItem(val id: String) { data class ProgramDataElement( val uid: String, - val program: String, - val programStage: String, + val program: String?, + val programStage: String?, val filters: List, - ) : TrackerLineListItem("$program.$programStage.$uid") + ) : TrackerLineListItem("${program?.let { "$it." } ?: ""}${programStage?.let { "$it." } ?: ""}$uid") object CreatedBy : TrackerLineListItem(Label.CreatedBy) object LastUpdatedBy : TrackerLineListItem(Label.LastUpdatedBy) - data class ProgramStatus(val filters: List) : TrackerLineListItem(Label.ProgramStatus) + data class ProgramStatusItem(val filters: List) : TrackerLineListItem(Label.ProgramStatus) + + data class EventStatusItem(val filters: List) : TrackerLineListItem(Label.EventStatus) +} - data class EventStatus(val filters: List) : TrackerLineListItem(Label.EventStatus) +sealed class OrganisationUnitFilter { + data class Absolute(val uid: String) : OrganisationUnitFilter() + data class Relative(val relative: RelativeOrganisationUnit) : OrganisationUnitFilter() + data class Level(val uid: String) : OrganisationUnitFilter() + data class Group(val uid: String) : OrganisationUnitFilter() } sealed class DateFilter { @@ -92,6 +96,7 @@ sealed class DataFilter { } internal object Label { + const val OrganisationUnit = "ou" const val LastUpdated = "lastUpdated" const val IncidentDate = "incidentDate" const val EnrollmentDate = "enrollmentDate" diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt index 7becf593f2..3f8396745b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -42,8 +42,7 @@ interface TrackerLineListRepository { fun withFilter(filter: TrackerLineListItem): TrackerLineListRepository - // TODO - fun withTrackerVisualization(): TrackerLineListRepository + fun withTrackerVisualization(trackerVisualization: String): TrackerLineListRepository fun evaluate(): Single> diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index ae377ff257..3ea5d91df0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -31,9 +31,27 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem internal data class TrackerLineListParams( + val trackerVisualization: String?, val outputType: TrackerLineListOutputType?, val programId: String?, val programStageId: String?, val columns: List, val filters: List, -) +) { + operator fun plus(other: TrackerLineListParams): TrackerLineListParams { + return other.copy( + outputType = other.outputType ?: outputType, + programId = other.programId ?: programId, + programStageId = other.programStageId ?: programStageId, + columns = other.columns.fold(columns) { list, item -> updateInList(list, item) }, + filters = other.filters.fold(filters) { list, item -> updateInList(list, item) }, + ) + } + + companion object { + fun updateInList(items: List, newItem: TrackerLineListItem): List { + val otherItems = items.filterNot { it.id == newItem.id } + return otherItems + newItem + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index 5b1308fbc9..e5b923d75a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListParams.Companion.updateInList import org.hisp.dhis.android.core.arch.helpers.Result import org.koin.core.annotation.Singleton @@ -62,16 +63,15 @@ internal class TrackerLineListRepositoryImpl( } override fun withColumn(column: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.copy(columns = updateItems(params.columns, column)) } + return updateParams { params.copy(columns = updateInList(params.columns, column)) } } override fun withFilter(filter: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.copy(columns = updateItems(params.filters, filter)) } + return updateParams { params.copy(columns = updateInList(params.filters, filter)) } } - // TODO - override fun withTrackerVisualization(): TrackerLineListRepositoryImpl { - return TODO() + override fun withTrackerVisualization(trackerVisualization: String): TrackerLineListRepositoryImpl { + return updateParams { params.copy(trackerVisualization = trackerVisualization) } } override fun evaluate(): Single> { @@ -87,9 +87,4 @@ internal class TrackerLineListRepositoryImpl( ): TrackerLineListRepositoryImpl { return TrackerLineListRepositoryImpl(func(params), service) } - - private fun updateItems(items: List, newItem: TrackerLineListItem): List { - val otherItems = items.filterNot { it.id == newItem.id } - return otherItems + newItem - } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index 1a19555dfb..aa4fae0282 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -40,47 +40,75 @@ import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.event.EventTableInfo +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationCollectionRepository import org.koin.core.annotation.Singleton @Singleton internal class TrackerLineListService( private val databaseAdapter: DatabaseAdapter, + private val trackerVisualizationCollectionRepository: TrackerVisualizationCollectionRepository, private val metadataHelper: TrackerLineListServiceMetadataHelper, + private val trackerVisualizationMapper: TrackerVisualizationMapper, ) { fun evaluate(params: TrackerLineListParams): Result { - // TODO Validate params + return try { + val evaluatedParams = evaluateParams(params) - val metadata = metadataHelper.getMetadata(params) + // TODO Validate params - val sqlClause = when (params.outputType!!) { - TrackerLineListOutputType.EVENT -> getEventSqlClause(params, metadata) - TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause() + val metadata = metadataHelper.getMetadata(evaluatedParams) + + val sqlClause = when (evaluatedParams.outputType!!) { + TrackerLineListOutputType.EVENT -> getEventSqlClause(evaluatedParams, metadata) + TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause() + } + + val cursor = databaseAdapter.rawQuery(sqlClause) + val values = mapCursorToColumns(evaluatedParams, cursor) + + Result.Success( + TrackerLineListResponse( + metadata = metadata, + headers = emptyList(), + filters = emptyList(), + rows = values, + ), + ) + } catch (e: AnalyticsException) { + Result.Failure(e) } + } + + private fun evaluateParams(params: TrackerLineListParams): TrackerLineListParams { + return if (params.trackerVisualization != null) { + val visualization = getTrackerVisualization(params.trackerVisualization) + ?: throw AnalyticsException.InvalidVisualization(params.trackerVisualization) + + trackerVisualizationMapper.toTrackerLineListParams(visualization) + params + } else { + params + } + } - val cursor = databaseAdapter.rawQuery(sqlClause) - val values = mapCursorToColumns(params, cursor) - - return Result.Success( - TrackerLineListResponse( - metadata = metadata, - headers = emptyList(), - filters = emptyList(), - rows = values, - ), - ) + private fun getTrackerVisualization(trackerVisualization: String): TrackerVisualization? { + return trackerVisualizationCollectionRepository + .withColumnsAndFilters() + .uid(trackerVisualization) + .blockingGet() } private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { return "SELECT " + - "${getEventSelectColumns(params, metadata)} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + - "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, metadata)} " + "${getEventSelectColumns(params, metadata)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, metadata)} " } private fun getEnrollmentSqlClause(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt new file mode 100644 index 0000000000..b53aa8287b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.dateRangeRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.orgunitGroupRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.orgunitLevelRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.uidRegex +import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.OrganisationUnitFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder +import org.hisp.dhis.android.core.common.RelativeOrganisationUnit +import org.hisp.dhis.android.core.common.RelativePeriod +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitLevelTableInfo +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelStore +import org.hisp.dhis.android.core.visualization.TrackerVisualization +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerVisualizationMapper( + private val organisationUnitLevelStore: OrganisationUnitLevelStore +) { + fun toTrackerLineListParams(trackerVisualization: TrackerVisualization): TrackerLineListParams { + return TrackerLineListParams( + trackerVisualization = trackerVisualization.uid(), + programId = trackerVisualization.program()?.uid(), + programStageId = trackerVisualization.programStage()?.uid(), + outputType = mapOutputType(trackerVisualization.outputType()), + columns = mapDimensions(trackerVisualization.columns()), + filters = mapDimensions(trackerVisualization.filters()), + ) + } + + private fun mapOutputType(type: TrackerVisualizationOutputType?): TrackerLineListOutputType? { + return when (type) { + TrackerVisualizationOutputType.ENROLLMENT -> TrackerLineListOutputType.ENROLLMENT + TrackerVisualizationOutputType.EVENT -> TrackerLineListOutputType.EVENT + else -> null + } + } + + private fun mapDimensions(dimensions: List?): List { + return dimensions?.mapNotNull { item -> + val mapper = when (item.dimensionType()) { + "ORGANISATION_UNIT" -> ::mapOrganisationUnit + "ORGANISATION_UNIT_GROUP_SET" -> ::mapOrganisationUnitGroup + "PERIOD" -> ::mapPeriod + "PROGRAM_INDICATOR" -> ::mapProgramIndicator + "PROGRAM_ATTRIBUTE" -> ::mapProgramAttribute + "PROGRAM_DATA_ELEMENT" -> ::mapProgramDataElement + "DATA_X" -> ::mapDataX + else -> { _ -> null } + } + mapper(item) + } ?: emptyList() + } + + private fun mapOrganisationUnit(item: TrackerVisualizationDimension): TrackerLineListItem? { + return TrackerLineListItem.OrganisationUnitItem( + filters = item.items()?.mapNotNull { it.uid() }?.mapNotNull { uid -> + val relativeOrgunit = RelativeOrganisationUnit.entries.find { it.name == uid } + + when { + relativeOrgunit != null -> { + OrganisationUnitFilter.Relative(relativeOrgunit) + } + + orgunitLevelRegex.matches(uid) -> { + val (levelNumber) = orgunitLevelRegex.find(uid)!!.destructured + val level = organisationUnitLevelStore.selectOneWhere( + WhereClauseBuilder() + .appendKeyNumberValue(OrganisationUnitLevelTableInfo.Columns.LEVEL, levelNumber.toInt()) + .build(), + ) ?: throw AnalyticsException.InvalidOrganisationUnitLevel(levelNumber) + OrganisationUnitFilter.Level(level.uid()) + } + + orgunitGroupRegex.matches(uid) -> { + val (groupUid) = orgunitGroupRegex.find(uid)!!.destructured + OrganisationUnitFilter.Group(groupUid) + } + + uidRegex.matches(uid) -> { + OrganisationUnitFilter.Absolute(uid) + } + + else -> null + } + } ?: emptyList() + ) + } + + private fun mapOrganisationUnitGroup(item: TrackerVisualizationDimension): TrackerLineListItem? { + // TODO + return null + } + + private fun mapPeriod(item: TrackerVisualizationDimension): TrackerLineListItem? { + return when (item.dimension()) { + "lastUpdated" -> TrackerLineListItem.DateItem.LastUpdated(mapDateFilters(item)) + "incidentDate" -> TrackerLineListItem.DateItem.IncidentDate(mapDateFilters(item)) + "enrollmentDate" -> TrackerLineListItem.DateItem.EnrollmentDate(mapDateFilters(item)) + "scheduledDate" -> TrackerLineListItem.DateItem.ScheduledDate(mapDateFilters(item)) + "eventDate" -> TrackerLineListItem.DateItem.EventDate(mapDateFilters(item)) + else -> null + } + } + + private fun mapProgramIndicator(item: TrackerVisualizationDimension): TrackerLineListItem? { + return item.dimension()?.let { uid -> + TrackerLineListItem.ProgramIndicator(uid, mapDataFilters(item)) + } + } + + private fun mapProgramAttribute(item: TrackerVisualizationDimension): TrackerLineListItem? { + return item.dimension()?.let { uid -> + TrackerLineListItem.ProgramAttribute(uid, mapDataFilters(item)) + } + } + + private fun mapProgramDataElement(item: TrackerVisualizationDimension): TrackerLineListItem? { + return item.dimension()?.let { uid -> + TrackerLineListItem + .ProgramDataElement(uid, item.program()?.uid(), item.programStage()?.uid(), mapDataFilters(item)) + } + } + + private fun mapDataX(item: TrackerVisualizationDimension): TrackerLineListItem? { + return when (item.dimension()) { + "createdBy" -> TrackerLineListItem.CreatedBy + "lastUpdatedBy" -> TrackerLineListItem.LastUpdatedBy + "programStatus" -> TrackerLineListItem.ProgramStatusItem( + filters = item.items()?.mapNotNull { e -> EnrollmentStatus.entries.find { it.name == e.uid() } } + ?: emptyList() + ) + + "eventStatus" -> TrackerLineListItem.EventStatusItem( + filters = item.items()?.mapNotNull { e -> EventStatus.entries.find { it.name == e.uid() } } + ?: emptyList() + ) + + else -> null + } + } + + private fun mapDataFilters(item: TrackerVisualizationDimension): List { + return if (item.filter().isNullOrEmpty()) { + emptyList() + } else { + val filterPairs = item.filter()!!.split(":").chunked(2) + + filterPairs.mapNotNull { filterPair -> + val operator = filterPair.getOrNull(0) + val value = filterPair.getOrNull(1) + if (operator != null && value != null) { + when (operator) { + "EQ" -> DataFilter.EqualTo(value) + "!EQ" -> DataFilter.NotEqualTo(value) + "IEQ" -> DataFilter.EqualToIgnoreCase(value) + "!IEQ" -> DataFilter.NotEqualToIgnoreCase(value) + "GT" -> DataFilter.GreaterThan(value) + "GE" -> DataFilter.GreaterThanOrEqualTo(value) + "LT" -> DataFilter.LowerThan(value) + "LE" -> DataFilter.LowerThanOrEqualTo(value) + "NE" -> DataFilter.NotEqualTo(value) + "LIKE" -> DataFilter.Like(value) + "!LIKE" -> DataFilter.NotLike(value) + "ILIKE" -> DataFilter.LikeIgnoreCase(value) + "!ILIKE" -> DataFilter.NotLikeIgnoreCase(value) + "IN" -> DataFilter.In(value.split(";")) + else -> null + } + } else { + null + } + } + } + } + + private fun mapDateFilters(item: TrackerVisualizationDimension): List { + return item.items()?.mapNotNull { it.uid() }?.map { uid -> + val relativePeriod = RelativePeriod.entries.find { it.name == uid } + + when { + relativePeriod != null -> { + DateFilter.Relative(relativePeriod) + } + + dateRangeRegex.matches(uid) -> { + val (start, end) = dateRangeRegex.find(uid)!!.destructured + DateFilter.Range(start, end) + } + + else -> { + DateFilter.Absolute(uid) + } + } + } ?: emptyList() + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegexShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegexShould.kt new file mode 100644 index 0000000000..1cce14733b --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegexShould.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.internal + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.dateRangeRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.orgunitLevelRegex +import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.uidRegex +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class AnalyticsRegexShould { + + @Test + fun should_evaluate_orgunit_regex() { + assertThat(orgunitLevelRegex.matches("LEVEL-4")).isTrue() + assertThat(orgunitLevelRegex.matches("LEVEL-ajflkaaf")).isFalse() + + val (level) = orgunitLevelRegex.find("LEVEL-5")!!.destructured + assertThat(level).isEqualTo("5") + } + + @Test + fun should_evaluate_date_range() { + assertThat(dateRangeRegex.matches("2015-12-11_2024-01-04")).isTrue() + assertThat(dateRangeRegex.matches("2015-12-11")).isFalse() + + val (start, end) = dateRangeRegex.find("2015-12-11_2024-01-04")!!.destructured + assertThat(start).isEqualTo("2015-12-11") + assertThat(end).isEqualTo("2024-01-04") + } + + @Test + fun should_evaluate_uid() { + assertThat(uidRegex.matches("YuQRtpL10I")).isFalse() + assertThat(uidRegex.matches("YuQRtpLP10I")).isTrue() + assertThat(uidRegex.matches("YuQRtpL10Ier")).isFalse() + } +} From 31aaf2f503a6d736dee619a3183a650d09f55d63 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 19 Feb 2024 10:01:52 +0100 Subject: [PATCH 094/222] [ANDROSDK-1811] Add integration tests --- .idea/gradle.xml | 3 +- ...llectionRepositoryMockIntegrationShould.kt | 2 +- .../core/analytics/internal/AnalyticsRegex.kt | 2 +- .../internal/TrackerLineListParams.kt | 25 +++-- .../internal/TrackerLineListRepositoryImpl.kt | 5 +- .../internal/TrackerLineListService.kt | 20 ++-- .../internal/TrackerVisualizationMapper.kt | 25 +++-- .../settings/AnalyticsDhisVisualization.java | 1 - .../externalmap}/external_map_layers.json | 0 .../TrackerLineListRepositoryShould.kt | 2 +- .../internal/TrackerLineListParamsShould.kt | 83 +++++++++++++++ .../TrackerVisualizationMapperShould.kt | 100 ++++++++++++++++++ .../core/settings/AnalyticsSettingAsserts.kt | 4 +- 13 files changed, 230 insertions(+), 42 deletions(-) rename core/src/sharedTest/resources/{map.layer.externalmap => map/layer/externalmap}/external_map_layers.json (100%) create mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index fdc5465974..b77a8dcd0d 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,8 +4,6 @@ diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt index 854b3b2b6a..02fd5b6ed9 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/map/layer/MapLayerCollectionRepositoryMockIntegrationShould.kt @@ -101,7 +101,7 @@ class MapLayerCollectionRepositoryMockIntegrationShould : BaseMockIntegrationTes .byMapLayerPosition().eq(MapLayerPosition.BASEMAP) .blockingGet() - assertThat(mapLayers.size).isEqualTo(9) + assertThat(mapLayers.size).isEqualTo(7) } @Test diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt index b382a9b7df..6410f656be 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsRegex.kt @@ -34,4 +34,4 @@ internal object AnalyticsRegex { val orgunitLevelRegex = "^LEVEL-(\\d+)\$".toRegex() val orgunitGroupRegex = "^OU_GROUP-(\\w{11})\$".toRegex() val dateRangeRegex = "^(\\d{4}-\\d{1,2}-\\d{1,2})_(\\d{4}-\\d{1,2}-\\d{1,2})\$".toRegex() -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index 3ea5d91df0..a9cab82004 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -39,19 +39,28 @@ internal data class TrackerLineListParams( val filters: List, ) { operator fun plus(other: TrackerLineListParams): TrackerLineListParams { - return other.copy( + return copy( outputType = other.outputType ?: outputType, programId = other.programId ?: programId, programStageId = other.programStageId ?: programStageId, - columns = other.columns.fold(columns) { list, item -> updateInList(list, item) }, - filters = other.filters.fold(filters) { list, item -> updateInList(list, item) }, + ).run { + other.columns.fold(this) { params, item -> params.pushToColumns(item) } + }.run { + other.filters.fold(this) { params, item -> params.pushToFilter(item) } + } + } + + fun pushToColumns(item: TrackerLineListItem): TrackerLineListParams { + return copy( + columns = columns.filterNot { it.id == item.id } + item, + filters = filters.filterNot { it.id == item.id }, ) } - companion object { - fun updateInList(items: List, newItem: TrackerLineListItem): List { - val otherItems = items.filterNot { it.id == newItem.id } - return otherItems + newItem - } + fun pushToFilter(item: TrackerLineListItem): TrackerLineListParams { + return copy( + columns = columns.filterNot { it.id == item.id }, + filters = filters.filterNot { it.id == item.id } + item, + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index e5b923d75a..3c65f41527 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -33,7 +33,6 @@ import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse -import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListParams.Companion.updateInList import org.hisp.dhis.android.core.arch.helpers.Result import org.koin.core.annotation.Singleton @@ -63,11 +62,11 @@ internal class TrackerLineListRepositoryImpl( } override fun withColumn(column: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.copy(columns = updateInList(params.columns, column)) } + return updateParams { params.pushToColumns(column) } } override fun withFilter(filter: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.copy(columns = updateInList(params.filters, filter)) } + return updateParams { params.pushToFilter(filter) } } override fun withTrackerVisualization(trackerVisualization: String): TrackerLineListRepositoryImpl { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index aa4fae0282..fa3afcc21c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -83,7 +83,7 @@ internal class TrackerLineListService( private fun evaluateParams(params: TrackerLineListParams): TrackerLineListParams { return if (params.trackerVisualization != null) { val visualization = getTrackerVisualization(params.trackerVisualization) - ?: throw AnalyticsException.InvalidVisualization(params.trackerVisualization) + ?: throw AnalyticsException.InvalidVisualization(params.trackerVisualization) trackerVisualizationMapper.toTrackerLineListParams(visualization) + params } else { @@ -100,15 +100,15 @@ internal class TrackerLineListService( private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { return "SELECT " + - "${getEventSelectColumns(params, metadata)} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + - "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, metadata)} " + "${getEventSelectColumns(params, metadata)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, metadata)} " } private fun getEnrollmentSqlClause(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index b53aa8287b..04fd586b16 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -50,8 +50,9 @@ import org.hisp.dhis.android.core.visualization.TrackerVisualizationOutputType import org.koin.core.annotation.Singleton @Singleton +@Suppress("TooManyFunctions") internal class TrackerVisualizationMapper( - private val organisationUnitLevelStore: OrganisationUnitLevelStore + private val organisationUnitLevelStore: OrganisationUnitLevelStore, ) { fun toTrackerLineListParams(trackerVisualization: TrackerVisualization): TrackerLineListParams { return TrackerLineListParams( @@ -76,12 +77,14 @@ internal class TrackerVisualizationMapper( return dimensions?.mapNotNull { item -> val mapper = when (item.dimensionType()) { "ORGANISATION_UNIT" -> ::mapOrganisationUnit - "ORGANISATION_UNIT_GROUP_SET" -> ::mapOrganisationUnitGroup "PERIOD" -> ::mapPeriod "PROGRAM_INDICATOR" -> ::mapProgramIndicator "PROGRAM_ATTRIBUTE" -> ::mapProgramAttribute "PROGRAM_DATA_ELEMENT" -> ::mapProgramDataElement "DATA_X" -> ::mapDataX + "ORGANISATION_UNIT_GROUP_SET" -> + throw AnalyticsException.InvalidArguments("Dimension ORGANISATION_UNIT_GROUP_SET IS not supported") + else -> { _ -> null } } mapper(item) @@ -119,15 +122,10 @@ internal class TrackerVisualizationMapper( else -> null } - } ?: emptyList() + } ?: emptyList(), ) } - private fun mapOrganisationUnitGroup(item: TrackerVisualizationDimension): TrackerLineListItem? { - // TODO - return null - } - private fun mapPeriod(item: TrackerVisualizationDimension): TrackerLineListItem? { return when (item.dimension()) { "lastUpdated" -> TrackerLineListItem.DateItem.LastUpdated(mapDateFilters(item)) @@ -158,25 +156,26 @@ internal class TrackerVisualizationMapper( } } - private fun mapDataX(item: TrackerVisualizationDimension): TrackerLineListItem? { + internal fun mapDataX(item: TrackerVisualizationDimension): TrackerLineListItem? { return when (item.dimension()) { "createdBy" -> TrackerLineListItem.CreatedBy "lastUpdatedBy" -> TrackerLineListItem.LastUpdatedBy "programStatus" -> TrackerLineListItem.ProgramStatusItem( filters = item.items()?.mapNotNull { e -> EnrollmentStatus.entries.find { it.name == e.uid() } } - ?: emptyList() + ?: emptyList(), ) "eventStatus" -> TrackerLineListItem.EventStatusItem( filters = item.items()?.mapNotNull { e -> EventStatus.entries.find { it.name == e.uid() } } - ?: emptyList() + ?: emptyList(), ) else -> null } } - private fun mapDataFilters(item: TrackerVisualizationDimension): List { + @Suppress("ComplexMethod") + internal fun mapDataFilters(item: TrackerVisualizationDimension): List { return if (item.filter().isNullOrEmpty()) { emptyList() } else { @@ -210,7 +209,7 @@ internal class TrackerVisualizationMapper( } } - private fun mapDateFilters(item: TrackerVisualizationDimension): List { + internal fun mapDateFilters(item: TrackerVisualizationDimension): List { return item.items()?.mapNotNull { it.uid() }?.map { uid -> val relativePeriod = RelativePeriod.entries.find { it.name == uid } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java index 2ebedb70d3..c06f310a32 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/AnalyticsDhisVisualization.java @@ -42,7 +42,6 @@ import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.AnalyticsDhisVisualizationScopeColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.enums.internal.AnalyticsDhisVisualizationTypeColumnAdapter; import org.hisp.dhis.android.core.common.CoreObject; -import org.hisp.dhis.android.core.common.ObjectStyle; import org.hisp.dhis.android.core.common.ObjectWithUidInterface; import static org.hisp.dhis.android.core.common.BaseIdentifiableObject.UID; diff --git a/core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json b/core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json similarity index 100% rename from core/src/sharedTest/resources/map.layer.externalmap/external_map_layers.json rename to core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt index c518fe0b36..7ceb7dae32 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt @@ -43,7 +43,7 @@ class TrackerLineListRepositoryShould { private val service: TrackerLineListService = mock() - private val initialParams = TrackerLineListParams(null, null, null, listOf(), listOf()) + private val initialParams = TrackerLineListParams(null, null, null, null, listOf(), listOf()) private val paramsCaptor = argumentCaptor() diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt new file mode 100644 index 0000000000..3ac47d43c7 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class TrackerLineListParamsShould { + + @Test + fun should_add_two_params() { + val params1 = TrackerLineListParams( + trackerVisualization = null, + outputType = TrackerLineListOutputType.EVENT, + programId = null, + programStageId = "program_stage_uid", + columns = listOf( + TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.GreaterThan("5"))), + TrackerLineListItem.ProgramIndicator("indicator", listOf()), + TrackerLineListItem.DateItem.EventDate(listOf()), + ), + filters = listOf(), + ) + + val params2 = TrackerLineListParams( + trackerVisualization = null, + outputType = null, + programId = "program_uid", + programStageId = null, + columns = listOf( + TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.NotEqualTo("10"))), + ), + filters = listOf( + TrackerLineListItem.DateItem.EventDate(listOf(DateFilter.Absolute("202405"))), + ), + ) + + val params = params1 + params2 + + assertThat(params.trackerVisualization).isNull() + assertThat(params.outputType).isEqualTo(TrackerLineListOutputType.EVENT) + assertThat(params.programId).isEqualTo("program_uid") + assertThat(params.programStageId).isEqualTo("program_stage_uid") + assertThat(params.columns).containsExactly( + TrackerLineListItem.ProgramIndicator("indicator", listOf()), + TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.NotEqualTo("10"))), + ) + assertThat(params.filters).containsExactly( + TrackerLineListItem.DateItem.EventDate(listOf(DateFilter.Absolute("202405"))), + ) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt new file mode 100644 index 0000000000..19b773ba3f --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import com.google.common.truth.Truth.assertThat +import com.nhaarman.mockitokotlin2.mock +import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelStore +import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class TrackerVisualizationMapperShould { + + private val organisationUniLevelStore: OrganisationUnitLevelStore = mock() + + private val mapper = TrackerVisualizationMapper(organisationUniLevelStore) + + @Test + fun should_map_data_filters() { + val item = TrackerVisualizationDimension.builder() + .filter("GT:6:ILIKE:ar:NE:4") + .build() + + val dataFilters = mapper.mapDataFilters(item) + + assertThat(dataFilters).containsExactly( + DataFilter.GreaterThan("6"), + DataFilter.LikeIgnoreCase("ar"), + DataFilter.NotEqualTo("4"), + ) + } + + @Test + fun should_map_date_filters() { + val item = TrackerVisualizationDimension.builder() + .items( + listOf( + ObjectWithUid.create("202403"), + ObjectWithUid.create("2024-03-05_2024-04-01"), + ), + ) + .build() + + val dateFilters = mapper.mapDateFilters(item) + + assertThat(dateFilters).containsExactly( + DateFilter.Absolute("202403"), + DateFilter.Range("2024-03-05", "2024-04-01"), + ) + } + + @Test + fun should_map_program_status() { + val item = TrackerVisualizationDimension.builder() + .dimensionType("dataX") + .dimension("programStatus") + .items( + listOf( + ObjectWithUid.create(EnrollmentStatus.ACTIVE.name), + ), + ) + .build() + + val programStatus = mapper.mapDataX(item) + + assertThat(programStatus).isEqualTo(TrackerLineListItem.ProgramStatusItem(listOf(EnrollmentStatus.ACTIVE))) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt index c4ea8abd85..4fa5972c72 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt @@ -99,7 +99,7 @@ object AnalyticsSettingAsserts { when (group.id()) { "12345678910" -> { assertThat(group.name()).isEqualTo("Ejemplo") - assertThat(group.visualizations().size).isEqualTo(2) + assertThat(group.visualizations().size).isEqualTo(3) } "12345678911" -> { assertThat(group.name()).isEqualTo("Otro ejemplo") @@ -128,4 +128,4 @@ object AnalyticsSettingAsserts { } } } -} \ No newline at end of file +} From 42aef62a3484dfe5c7505bd27fe87bb8ea7bdabe Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 19 Feb 2024 12:24:20 +0100 Subject: [PATCH 095/222] [ANDROSDK-1815] Do not open databases pending to import --- .../core/user/internal/AccountManagerImpl.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt index e9b69d877c..2933c0565f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt @@ -36,6 +36,7 @@ import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import org.hisp.dhis.android.core.arch.storage.internal.Credentials import org.hisp.dhis.android.core.arch.storage.internal.CredentialsSecureStore import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount +import org.hisp.dhis.android.core.configuration.internal.DatabaseAccountImportStatus import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationInsecureStore import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManager @@ -128,12 +129,16 @@ internal class AccountManagerImpl constructor( } private fun updateSyncState(account: DatabaseAccount): DatabaseAccount { - val databaseAdapter = databaseAdapterFactory.getDatabaseAdapter(account) - val syncState = AccountManagerHelper.getSyncState(databaseAdapter) + return if (account.importDB()?.status() != DatabaseAccountImportStatus.PENDING_TO_IMPORT) { + val databaseAdapter = databaseAdapterFactory.getDatabaseAdapter(account) + val syncState = AccountManagerHelper.getSyncState(databaseAdapter) - return account.toBuilder() - .syncState(syncState) - .build() + account.toBuilder() + .syncState(syncState) + .build() + } else { + account + } } override fun accountDeletionObservable(): Observable { From ff0e89b20c5740e3b97b620cc5f2e3968889b295 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 20 Feb 2024 10:09:56 +0100 Subject: [PATCH 096/222] [ANDROSDK-1811] Fix unit tests --- .../sharedTest/resources/settings/analytics_settings_v2.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/sharedTest/resources/settings/analytics_settings_v2.json b/core/src/sharedTest/resources/settings/analytics_settings_v2.json index 93cf13e54c..5ded36f26b 100644 --- a/core/src/sharedTest/resources/settings/analytics_settings_v2.json +++ b/core/src/sharedTest/resources/settings/analytics_settings_v2.json @@ -75,6 +75,10 @@ { "id": "PYBH8ZaAQnC", "timestamp": "2021-07-01T03:02:16.8770" + }, + { + "id": "s85urBIkN0z", + "timestamp": "2021-07-01T03:02:16.8770" } ] }, From 4d130a4c93e63b557fcc182ae8f9b7508c8e4971 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 21 Feb 2024 13:18:47 +0100 Subject: [PATCH 097/222] [ANDROSDK-1818] Avoid query to versionManager before login in OpenID login --- .../org/hisp/dhis/android/core/user/internal/LogInCall.kt | 6 ++---- .../org/hisp/dhis/android/core/user/internal/UserFields.kt | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt index 8a765801e3..8d8ebeba62 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt @@ -36,7 +36,6 @@ import org.hisp.dhis.android.core.arch.storage.internal.UserIdInMemoryStore import org.hisp.dhis.android.core.configuration.internal.ServerUrlParser import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode -import org.hisp.dhis.android.core.systeminfo.DHISVersionManager import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoCall import org.hisp.dhis.android.core.user.AccountDeletionReason import org.hisp.dhis.android.core.user.AuthenticatedUser @@ -58,7 +57,6 @@ internal class LogInCall( private val databaseManager: LogInDatabaseManager, private val exceptions: LogInExceptions, private val accountManager: AccountManagerImpl, - private val versionManager: DHISVersionManager, private val apiCallErrorCatcher: UserAuthenticateCallErrorCatcher, ) { suspend fun logIn(username: String?, password: String?, serverUrl: String?): User { @@ -85,7 +83,7 @@ internal class LogInCall( val user = coroutineAPICallExecutor.wrap(errorCatcher = apiCallErrorCatcher) { userService.authenticate( okhttp3.Credentials.basic(username, password!!), - UserFields.allFieldsWithoutOrgUnit(null), + UserFields.allFieldsWithoutOrgUnit, ) }.getOrThrow() loginOnline(user, credentials) @@ -189,7 +187,7 @@ internal class LogInCall( val user = coroutineAPICallExecutor.wrap(errorCatcher = apiCallErrorCatcher) { userService.authenticate( "Bearer ${openIDConnectState.idToken}", - UserFields.allFieldsWithoutOrgUnit(versionManager.getVersion()), + UserFields.allFieldsWithoutOrgUnit, ) }.getOrThrow() credentials = getOpenIdConnectCredentials(user, trimmedServerUrl!!, openIDConnectState) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt index b995cd96e1..dc3226d78a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt @@ -115,9 +115,7 @@ object UserFields { } } - fun allFieldsWithoutOrgUnit(version: DHISVersion?): Fields { - return getBaseFields(version).build() - } + val allFieldsWithoutOrgUnit: Fields = getBaseFields(null).build() fun allFieldsWithOrgUnit(version: DHISVersion?): Fields { return getBaseFields(version) From 4c2e12e0f5963233a07b3f55f34c66338a365e7b Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 22 Feb 2024 09:27:37 +0100 Subject: [PATCH 098/222] [ANDROSDK-1818] Adapt unit test --- .../dhis/android/core/user/internal/LogInCallUnitShould.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/user/internal/LogInCallUnitShould.kt b/core/src/test/java/org/hisp/dhis/android/core/user/internal/LogInCallUnitShould.kt index 00ae1f478e..1172a69e40 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/user/internal/LogInCallUnitShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/user/internal/LogInCallUnitShould.kt @@ -43,8 +43,6 @@ import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManage import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.settings.internal.GeneralSettingCall -import org.hisp.dhis.android.core.systeminfo.DHISVersion -import org.hisp.dhis.android.core.systeminfo.DHISVersionManager import org.hisp.dhis.android.core.systeminfo.SystemInfo import org.hisp.dhis.android.core.systeminfo.internal.SystemInfoCall import org.hisp.dhis.android.core.user.AuthenticatedUser @@ -80,7 +78,6 @@ class LogInCallUnitShould : BaseCallShould() { private val multiUserDatabaseManager: MultiUserDatabaseManager = mock() private val generalSettingCall: GeneralSettingCall = mock() private val accountManager: AccountManagerImpl = mock() - private val versionManager: DHISVersionManager = mock() @Before @Throws(Exception::class) @@ -107,8 +104,6 @@ class LogInCallUnitShould : BaseCallShould() { generalSettingCall.stub { onBlocking { isDatabaseEncrypted() }.doReturn(false) } - - whenever(versionManager.getVersion()).thenReturn(DHISVersion.V2_39) } private suspend fun login() = instantiateCall(USERNAME, PASSWORD, serverUrl) @@ -118,7 +113,7 @@ class LogInCallUnitShould : BaseCallShould() { coroutineAPICallExecutor, userService, credentialsSecureStore, userIdStore, userHandler, authenticatedUserStore, systemInfoCall, userStore, LogInDatabaseManager(multiUserDatabaseManager, generalSettingCall), - LogInExceptions(credentialsSecureStore), accountManager, versionManager, apiErrorCatcher, + LogInExceptions(credentialsSecureStore), accountManager, apiErrorCatcher, ).logIn(username, password, serverUrl) } From b999bf8e0d7d10ba16c4e07a2cd1ede2a931c520 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 22 Feb 2024 10:13:32 +0100 Subject: [PATCH 099/222] [ANDROSDK-1806-2] TrackerTerminology - add more labels to Program --- ...ollectionRepositoryMockIntegrationShould.java | 16 ++++++++++++++++ core/src/main/assets/migrations/157.sql | 2 ++ core/src/main/assets/snapshots/snapshot.sql | 2 +- .../hisp/dhis/android/core/program/Program.java | 12 ++++++++++++ .../core/program/ProgramCollectionRepository.kt | 8 ++++++++ .../android/core/program/ProgramTableInfo.java | 6 +++++- .../core/program/internal/ProgramFields.kt | 2 ++ .../core/program/internal/ProgramStoreImpl.kt | 2 ++ .../core/data/program/ProgramSamples.java | 5 ++++- .../sharedTest/resources/program/program.json | 2 ++ .../sharedTest/resources/program/programs.json | 2 ++ .../dhis/android/core/program/ProgramShould.kt | 2 ++ 12 files changed, 58 insertions(+), 3 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java index 1f2af2fde7..879ad821f4 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/program/ProgramCollectionRepositoryMockIntegrationShould.java @@ -243,6 +243,22 @@ public void filter_by_tracked_entity_attribute_label() { assertThat(programs.size()).isEqualTo(1); } + @Test + public void filter_by_program_stage_label() { + List programs = d2.programModule().programs() + .byProgramStageLabel().eq("ProgramStage Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + + @Test + public void filter_by_event_label() { + List programs = d2.programModule().programs() + .byEventLabel().eq("Event Label") + .blockingGet(); + assertThat(programs.size()).isEqualTo(1); + } + @Test public void filter_by_field_color() { List programs = d2.programModule().programs() diff --git a/core/src/main/assets/migrations/157.sql b/core/src/main/assets/migrations/157.sql index e299a6a117..7167d8c2f8 100644 --- a/core/src/main/assets/migrations/157.sql +++ b/core/src/main/assets/migrations/157.sql @@ -6,6 +6,8 @@ ALTER TABLE Program ADD COLUMN orgUnitLabel TEXT; ALTER TABLE Program ADD COLUMN relationshipLabel TEXT; ALTER TABLE Program ADD COLUMN noteLabel TEXT; ALTER TABLE Program ADD COLUMN trackedEntityAttributeLabel TEXT; +ALTER TABLE Program ADD COLUMN programStageLabel TEXT; +ALTER TABLE Program ADD COLUMN eventLabel TEXT; ALTER TABLE ProgramStage ADD COLUMN programStageLabel TEXT; ALTER TABLE ProgramStage ADD COLUMN eventLabel TEXT; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index df3124a9bb..1d2f4cc6b7 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -66,7 +66,7 @@ CREATE TABLE DataSetOrganisationUnitLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, CREATE TABLE UserOrganisationUnit (_id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT NOT NULL, organisationUnit TEXT NOT NULL, organisationUnitScope TEXT NOT NULL, root INTEGER, userAssigned INTEGER, FOREIGN KEY (user) REFERENCES User (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, UNIQUE (organisationUnitScope, user, organisationUnit)); CREATE TABLE RelationshipType (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, fromToName TEXT, toFromName TEXT, bidirectional INTEGER, accessDataWrite INTEGER ); CREATE TABLE ProgramStage (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, executionDateLabel TEXT, allowGenerateNextVisit INTEGER, validCompleteOnly INTEGER, reportDateToUse TEXT, openAfterEnrollment INTEGER, repeatable INTEGER, formType TEXT, displayGenerateEventBox INTEGER, generatedByEnrollmentDate INTEGER, autoGenerateEvent INTEGER, sortOrder INTEGER, hideDueDate INTEGER, blockEntryForm INTEGER, minDaysFromStart INTEGER, standardInterval INTEGER, program TEXT NOT NULL, periodType TEXT, accessDataWrite INTEGER, remindCompleted INTEGER, description TEXT, displayDescription TEXT, featureType TEXT, color TEXT, icon TEXT, enableUserAssignment INTEGER, dueDateLabel TEXT, validationStrategy TEXT, programStageLabel TEXT, eventLabel TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE Program (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, shortName TEXT, displayShortName TEXT, description TEXT, displayDescription TEXT, version INTEGER, onlyEnrollOnce INTEGER, enrollmentDateLabel TEXT, displayIncidentDate INTEGER, incidentDateLabel TEXT, registration INTEGER, selectEnrollmentDatesInFuture INTEGER, dataEntryMethod INTEGER, ignoreOverdueEvents INTEGER, selectIncidentDatesInFuture INTEGER, useFirstStageDuringRegistration INTEGER, displayFrontPageList INTEGER, programType TEXT, relatedProgram TEXT, trackedEntityType TEXT, categoryCombo TEXT, accessDataWrite INTEGER, expiryDays INTEGER, completeEventsExpiryDays INTEGER, expiryPeriodType TEXT, minAttributesRequiredToSearch INTEGER, maxTeiCountToReturn INTEGER, featureType TEXT, accessLevel TEXT, color TEXT, icon TEXT, enrollmentLabel TEXT, followUpLabel TEXT, orgUnitLabel TEXT, relationshipLabel TEXT, noteLabel TEXT, trackedEntityAttributeLabel TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (categoryCombo) REFERENCES CategoryCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); +CREATE TABLE Program (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, shortName TEXT, displayShortName TEXT, description TEXT, displayDescription TEXT, version INTEGER, onlyEnrollOnce INTEGER, enrollmentDateLabel TEXT, displayIncidentDate INTEGER, incidentDateLabel TEXT, registration INTEGER, selectEnrollmentDatesInFuture INTEGER, dataEntryMethod INTEGER, ignoreOverdueEvents INTEGER, selectIncidentDatesInFuture INTEGER, useFirstStageDuringRegistration INTEGER, displayFrontPageList INTEGER, programType TEXT, relatedProgram TEXT, trackedEntityType TEXT, categoryCombo TEXT, accessDataWrite INTEGER, expiryDays INTEGER, completeEventsExpiryDays INTEGER, expiryPeriodType TEXT, minAttributesRequiredToSearch INTEGER, maxTeiCountToReturn INTEGER, featureType TEXT, accessLevel TEXT, color TEXT, icon TEXT, enrollmentLabel TEXT, followUpLabel TEXT, orgUnitLabel TEXT, relationshipLabel TEXT, noteLabel TEXT, trackedEntityAttributeLabel TEXT, programStageLabel TEXT, eventLabel TEXT, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (categoryCombo) REFERENCES CategoryCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE TrackedEntityInstance (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, organisationUnit TEXT, trackedEntityType TEXT, geometryType TEXT, geometryCoordinates TEXT, syncState TEXT, aggregatedSyncState TEXT, deleted INTEGER, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE Enrollment (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, organisationUnit TEXT NOT NULL, program TEXT NOT NULL, enrollmentDate TEXT, incidentDate TEXT, followup INTEGER, status TEXT, trackedEntityInstance TEXT NOT NULL, syncState TEXT, aggregatedSyncState TEXT, geometryType TEXT, geometryCoordinates TEXT, deleted INTEGER, completedDate TEXT, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityInstance) REFERENCES TrackedEntityInstance (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE Event (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, enrollment TEXT, created TEXT, lastUpdated TEXT, createdAtClient TEXT, lastUpdatedAtClient TEXT, status TEXT, geometryType TEXT, geometryCoordinates TEXT, program TEXT NOT NULL, programStage TEXT NOT NULL, organisationUnit TEXT NOT NULL, eventDate TEXT, completedDate TEXT, dueDate TEXT, syncState TEXT, aggregatedSyncState TEXT, attributeOptionCombo TEXT, deleted INTEGER, assignedUser TEXT, completedBy TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (enrollment) REFERENCES Enrollment (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (organisationUnit) REFERENCES OrganisationUnit (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (attributeOptionCombo) REFERENCES CategoryOptionCombo (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java index f5a4afbe6d..145c6c3a52 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/Program.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/Program.java @@ -227,6 +227,14 @@ public String categoryComboUid() { @JsonProperty() public abstract String trackedEntityAttributeLabel(); + @Nullable + @JsonProperty() + public abstract String programStageLabel(); + + @Nullable + @JsonProperty() + public abstract String eventLabel(); + @Nullable @JsonProperty() @ColumnAdapter(IgnoreAttributeValuesListAdapter.class) @@ -322,6 +330,10 @@ abstract Builder programTrackedEntityAttributes(List attributeValues); abstract Program autoBuild(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt index ba494fea01..4a8f475cb8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramCollectionRepository.kt @@ -189,6 +189,14 @@ class ProgramCollectionRepository internal constructor( return cf.string(ProgramTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE_LABEL) } + fun byProgramStageLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.PROGRAM_STAGE_LABEL) + } + + fun byEventLabel(): StringFilterConnector { + return cf.string(ProgramTableInfo.Columns.EVENT_LABEL) + } + fun byColor(): StringFilterConnector { return cf.string(ProgramTableInfo.Columns.COLOR) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java index ed5674245b..de8073df57 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/ProgramTableInfo.java @@ -81,6 +81,8 @@ public static class Columns extends NameableWithStyleColumns { public static final String RELATIONSHIP_LABEL = "relationshipLabel"; public static final String NOTE_LABEL = "noteLabel"; public static final String TRACKED_ENTITY_ATTRIBUTE_LABEL = "trackedEntityAttributeLabel"; + public static final String PROGRAM_STAGE_LABEL = "programStageLabel"; + public static final String EVENT_LABEL = "eventLabel"; @Override public String[] all() { @@ -114,7 +116,9 @@ public String[] all() { ORG_UNIT_LABEL, RELATIONSHIP_LABEL, NOTE_LABEL, - TRACKED_ENTITY_ATTRIBUTE_LABEL + TRACKED_ENTITY_ATTRIBUTE_LABEL, + PROGRAM_STAGE_LABEL, + EVENT_LABEL ); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt index e8bbf07a1a..89b232d859 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramFields.kt @@ -101,5 +101,7 @@ internal object ProgramFields { fh.field(ProgramTableInfo.Columns.RELATIONSHIP_LABEL), fh.field(ProgramTableInfo.Columns.NOTE_LABEL), fh.field(ProgramTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE_LABEL), + fh.field(ProgramTableInfo.Columns.PROGRAM_STAGE_LABEL), + fh.field(ProgramTableInfo.Columns.EVENT_LABEL), ).build() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt index db751d727f..03d989552c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStoreImpl.kt @@ -93,6 +93,8 @@ internal class ProgramStoreImpl( w.bind(40, o.relationshipLabel()) w.bind(41, o.noteLabel()) w.bind(42, o.trackedEntityAttributeLabel()) + w.bind(43, o.programStageLabel()) + w.bind(44, o.eventLabel()) } } } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java index 594b273ca4..8a097d7751 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java @@ -81,6 +81,8 @@ public static Program getProgram() { .relationshipLabel("relationshipLabel") .noteLabel("noteLabel") .trackedEntityAttributeLabel("trackedEntityAttributeLabel") + .programStageLabel("programStageLabel") + .eventLabel("eventLabel") .build(); return builder.build(); } @@ -125,7 +127,8 @@ public static Program getAntenatalProgram() { .relationshipLabel("Relationship Label") .noteLabel("Note Label") .trackedEntityAttributeLabel("TrackedEntityAttribute Label") - + .programStageLabel("programStageLabel") + .eventLabel("eventLabel") .build(); } diff --git a/core/src/sharedTest/resources/program/program.json b/core/src/sharedTest/resources/program/program.json index 9c33d95bf1..c59108030b 100644 --- a/core/src/sharedTest/resources/program/program.json +++ b/core/src/sharedTest/resources/program/program.json @@ -20,6 +20,8 @@ "relationshipLabel": "Relationship Label", "noteLabel": "Note Label", "trackedEntityAttributeLabel": "TrackedEntityAttribute Label", + "programStageLabel": "ProgramStage Label", + "eventLabel": "Event Label", "selectEnrollmentDatesInFuture": false, "registration": true, "useFirstStageDuringRegistration": false, diff --git a/core/src/sharedTest/resources/program/programs.json b/core/src/sharedTest/resources/program/programs.json index 294b76dce6..091e96d7ed 100644 --- a/core/src/sharedTest/resources/program/programs.json +++ b/core/src/sharedTest/resources/program/programs.json @@ -26,6 +26,8 @@ "relationshipLabel": "Relationship Label", "noteLabel": "Note Label", "trackedEntityAttributeLabel": "TrackedEntityAttribute Label", + "programStageLabel": "ProgramStage Label", + "eventLabel": "Event Label", "displayIncidentDate": false, "selectEnrollmentDatesInFuture": false, "registration": false, diff --git a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt index 5b55c17c63..6c2d6ebf0c 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/program/ProgramShould.kt @@ -67,6 +67,8 @@ class ProgramShould : BaseObjectShould("program/program.json"), ObjectShould { assertThat(program.relationshipLabel()).isEqualTo("Relationship Label") assertThat(program.noteLabel()).isEqualTo("Note Label") assertThat(program.trackedEntityAttributeLabel()).isEqualTo("TrackedEntityAttribute Label") + assertThat(program.programStageLabel()).isEqualTo("ProgramStage Label") + assertThat(program.eventLabel()).isEqualTo("Event Label") assertThat(program.displayFrontPageList()).isFalse() assertThat(program.programType()).isEqualTo(ProgramType.WITH_REGISTRATION) assertThat(program.displayIncidentDate()).isFalse() From c9bf5b7d799201042bd6d439a83050c750b099b2 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 23 Feb 2024 10:43:29 +0100 Subject: [PATCH 100/222] [ANDROSDK-1806-2] Fix integration test --- .../hisp/dhis/android/core/data/program/ProgramSamples.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java index 8a097d7751..4fbe286813 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/program/ProgramSamples.java @@ -127,8 +127,8 @@ public static Program getAntenatalProgram() { .relationshipLabel("Relationship Label") .noteLabel("Note Label") .trackedEntityAttributeLabel("TrackedEntityAttribute Label") - .programStageLabel("programStageLabel") - .eventLabel("eventLabel") + .programStageLabel("ProgramStage Label") + .eventLabel("Event Label") .build(); } From 59932d9fddb59c49af1ba1a298ee28cee24986af Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 11:04:38 +0100 Subject: [PATCH 101/222] [ANDROSDK-1817] Update gradle version and remove targetSdk --- core/build.gradle.kts | 1 - gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d85e4cc5f4..1776d303dc 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -53,7 +53,6 @@ android { defaultConfig { minSdk = libs.versions.minSdkVersion.get().toInt() - targetSdk = libs.versions.targetSdkVersion.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true vectorDrawables.useSupportLibrary = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed9329f188..6f5e1080c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -gradle = "8.2.0" +gradle = "8.2.2" kotlin = "1.9.21" ktlint = "11.5.1" jacoco = "0.8.10" From 0c58f66c9bd2e5abe6c64ca05d2fbffc40f22c54 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:46:16 +0100 Subject: [PATCH 102/222] [ANDROSDK-1817] Add UserGroup pojo --- .../dhis/android/core/user/UserGroup.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java new file mode 100644 index 0000000000..885911dd74 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.user; + +import android.database.Cursor; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +import org.hisp.dhis.android.core.common.BaseIdentifiableObject; +import org.hisp.dhis.android.core.common.CoreObject; + +@AutoValue +@JsonDeserialize(builder = AutoValue_UserGroup.Builder.class) +public abstract class UserGroup extends BaseIdentifiableObject implements CoreObject { + + public static Builder builder() { + return new $$AutoValue_UserGroup.Builder(); + } + + public static UserGroup create(Cursor cursor) { + return $AutoValue_UserGroup.createFromCursor(cursor); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") + public abstract static class Builder extends BaseIdentifiableObject.Builder { + public abstract Builder id(Long id); + + public abstract UserGroup build(); + } +} \ No newline at end of file From 71dc6c519101f6e0aa417b66a6ee2256dcfa8afc Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:47:01 +0100 Subject: [PATCH 103/222] [ANDROSDK-1817] Add UserGroup table info --- .../android/core/user/UserGroupTableInfo.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt new file mode 100644 index 0000000000..fa143359f0 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user + +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.common.CoreColumns +import org.hisp.dhis.android.core.common.IdentifiableColumns + +object UserGroupTableInfo { + + @JvmField + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "UserGroup" + } + + override fun columns(): CoreColumns { + return Columns() + } + } + + class Columns : IdentifiableColumns() +} \ No newline at end of file From a324dc8aeaf753480c9a06339a69617a282a254e Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:47:20 +0100 Subject: [PATCH 104/222] [ANDROSDK-1817] Add UserGroup store --- .../core/user/internal/UserGroupStore.kt | 34 ++++++++++++ .../core/user/internal/UserGroupStoreImpl.kt | 52 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStore.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreImpl.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStore.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStore.kt new file mode 100644 index 0000000000..cddcc80f4d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore +import org.hisp.dhis.android.core.user.UserGroup + +internal interface UserGroupStore : IdentifiableObjectStore diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreImpl.kt new file mode 100644 index 0000000000..df6ec22e4c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreImpl.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.IdentifiableStatementBinder +import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinder +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStoreImpl +import org.hisp.dhis.android.core.user.UserGroup +import org.hisp.dhis.android.core.user.UserGroupTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class UserGroupStoreImpl( + databaseAdapter: DatabaseAdapter, +) : UserGroupStore, + IdentifiableObjectStoreImpl( + databaseAdapter, + UserGroupTableInfo.TABLE_INFO, + BINDER, + { cursor: Cursor -> UserGroup.create(cursor) }, + ) { + companion object { + private val BINDER: StatementBinder = object : IdentifiableStatementBinder() {} + } +} From 987baa6a694a85f046b5639774da8a37624b375d Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:47:44 +0100 Subject: [PATCH 105/222] [ANDROSDK-1817] Add UserGroup handle --- .../core/user/internal/UserGroupFields.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt new file mode 100644 index 0000000000..5d496c87e9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.arch.api.fields.internal.Fields +import org.hisp.dhis.android.core.arch.fields.internal.FieldsHelper +import org.hisp.dhis.android.core.user.UserGroup + +internal object UserGroupFields { + private val fh = FieldsHelper() + val allFields: Fields = Fields.builder() + .fields(fh.getIdentifiableFields()) + .build() +} \ No newline at end of file From 8f4f09cbb5510fb3969cf4955271ad421d9b0493 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:48:17 +0100 Subject: [PATCH 106/222] [ANDROSDK-1817] Add UserGroup fields --- .../internal/UserGroupCollectionCleaner.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupCollectionCleaner.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupCollectionCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupCollectionCleaner.kt new file mode 100644 index 0000000000..420d1f6478 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupCollectionCleaner.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.arch.cleaners.internal.CollectionCleanerImpl +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.user.UserGroup +import org.hisp.dhis.android.core.user.UserGroupTableInfo +import org.koin.core.annotation.Singleton + +@Singleton +internal class UserGroupCollectionCleaner( + databaseAdapter: DatabaseAdapter, +) : CollectionCleanerImpl( + tableName = UserGroupTableInfo.TABLE_INFO.name(), + databaseAdapter = databaseAdapter, +) From b5cdf390616be3b21ca241c1c80a4c144ff1710a Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:48:55 +0100 Subject: [PATCH 107/222] [ANDROSDK-1817] Add UserGroup collection repository --- .../IgnoreUserGroupListColumnAdapter.kt | 32 ++++++++++ .../user/UserGroupCollectionRepository.kt | 61 +++++++++++++++++++ .../internal/UserGroupChildrenAppender.kt | 46 ++++++++++++++ .../core/user/internal/UserGroupHandler.kt | 38 ++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/UserGroupCollectionRepository.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupChildrenAppender.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupHandler.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt new file mode 100644 index 0000000000..8678d358ea --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal + +import org.hisp.dhis.android.core.user.UserGroup + +class IgnoreUserGroupListColumnAdapter : IgnoreColumnAdapter>() diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupCollectionRepository.kt new file mode 100644 index 0000000000..5441c2ef8d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupCollectionRepository.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppenderGetter +import org.hisp.dhis.android.core.arch.repositories.collection.internal.ReadOnlyIdentifiableCollectionRepositoryImpl +import org.hisp.dhis.android.core.arch.repositories.filters.internal.FilterConnectorFactory +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.user.internal.UserGroupStore +import org.koin.core.annotation.Singleton + +@Singleton +class UserGroupCollectionRepository internal constructor( + store: UserGroupStore, + databaseAdapter: DatabaseAdapter, + scope: RepositoryScope, +) : ReadOnlyIdentifiableCollectionRepositoryImpl( + store, + databaseAdapter, + childrenAppenders, + scope, + FilterConnectorFactory( + scope, + ) { s: RepositoryScope -> + UserGroupCollectionRepository( + store, + databaseAdapter, + s, + ) + }, +) { + internal companion object { + val childrenAppenders: ChildrenAppenderGetter = emptyMap() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupChildrenAppender.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupChildrenAppender.kt new file mode 100644 index 0000000000..437eca2457 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupChildrenAppender.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter +import org.hisp.dhis.android.core.arch.repositories.children.internal.ChildrenAppender +import org.hisp.dhis.android.core.user.User +import org.koin.core.annotation.Singleton + +@Singleton +internal class UserGroupChildrenAppender( + databaseAdapter: DatabaseAdapter, +) : ChildrenAppender() { + private val store = UserGroupStoreImpl(databaseAdapter) + + override fun appendChildren(user: User): User { + val builder = user.toBuilder() + builder.userGroups(store.selectAll()) + return builder.build() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupHandler.kt new file mode 100644 index 0000000000..79bb683912 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupHandler.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl +import org.hisp.dhis.android.core.user.UserGroup +import org.koin.core.annotation.Singleton + +@Singleton +internal class UserGroupHandler( + store: UserGroupStore, +) : IdentifiableHandlerImpl(store) From c587f4acd466eb6dccf2e5a4aa874cfab3e2ed75 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:49:14 +0100 Subject: [PATCH 108/222] [ANDROSDK-1817] Add UserGroup migration and snapshot --- core/src/main/assets/migrations/162.sql | 3 +++ core/src/main/assets/snapshots/snapshot.sql | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 core/src/main/assets/migrations/162.sql diff --git a/core/src/main/assets/migrations/162.sql b/core/src/main/assets/migrations/162.sql new file mode 100644 index 0000000000..03cf8d0aae --- /dev/null +++ b/core/src/main/assets/migrations/162.sql @@ -0,0 +1,3 @@ +# Add UserGroup (ANDROSDK-1817) + +CREATE TABLE UserGroup (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT); diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index df3124a9bb..d285c482d8 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -132,4 +132,5 @@ CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadUR CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); \ No newline at end of file +CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); +CREATE TABLE UserGroup (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT); \ No newline at end of file From b9089afb033558d7a510d23b664e8e055263c2c8 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:49:34 +0100 Subject: [PATCH 109/222] [ANDROSDK-1817] Update db version --- .../core/arch/db/access/internal/BaseDatabaseOpenHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index e8a584ea5a..bcae0fe711 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 161 + const val VERSION = 162 } } From 8caf6f5ee553001368a9c63a5fbf98235de7847d Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:49:52 +0100 Subject: [PATCH 110/222] [ANDROSDK-1817] Adapt User and User handler --- .../main/java/org/hisp/dhis/android/core/user/User.java | 8 ++++++++ .../hisp/dhis/android/core/user/internal/UserFields.kt | 4 ++++ .../hisp/dhis/android/core/user/internal/UserHandler.kt | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/User.java b/core/src/main/java/org/hisp/dhis/android/core/user/User.java index 9aeb3d0caf..ae0f7c7bfc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/User.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/User.java @@ -38,6 +38,7 @@ import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreOrganisationUnitListAdapter; import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreUserCredentialsAdapter; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreUserGroupListColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreUserRoleListColumnAdapter; import org.hisp.dhis.android.core.common.BaseIdentifiableObject; import org.hisp.dhis.android.core.common.CoreObject; @@ -111,6 +112,11 @@ public abstract class User extends BaseIdentifiableObject implements CoreObject @ColumnAdapter(IgnoreUserRoleListColumnAdapter.class) public abstract List userRoles(); + @Nullable + @JsonProperty() + @ColumnAdapter(IgnoreUserGroupListColumnAdapter.class) + public abstract List userGroups(); + public abstract Builder toBuilder(); public static Builder builder() { @@ -164,6 +170,8 @@ public abstract static class Builder extends BaseIdentifiableObject.Builder userRoles); + public abstract Builder userGroups(List userGroups); + public abstract User build(); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt index dc3226d78a..063dd38fed 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserFields.kt @@ -36,6 +36,7 @@ import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitFiel import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.UserCredentials +import org.hisp.dhis.android.core.user.UserGroup import org.hisp.dhis.android.core.user.UserRole object UserFields { @@ -55,6 +56,7 @@ object UserFields { const val NATIONALITY = "nationality" const val USER_CREDENTIALS = "userCredentials" const val USER_ROLES = "userRoles" + const val USER_GROUPS = "userGroups" private const val ORGANISATION_UNITS = "organisationUnits" private const val TEI_SEARCH_ORGANISATION_UNITS = "teiSearchOrganisationUnits" @@ -83,11 +85,13 @@ object UserFields { private val organisationUnits = NestedField.create(ORGANISATION_UNITS) private val teiSearchOrganisationUnits = NestedField.create(TEI_SEARCH_ORGANISATION_UNITS) private val userRoles = NestedField.create(USER_ROLES) + private val userGroups = NestedField.create(USER_GROUPS) private fun commonFields(): Fields.Builder { return Fields.builder().fields( uid, code, name, displayName, created, lastUpdated, birthday, education, gender, jobTitle, surname, firstName, introduction, employer, interests, languages, email, phoneNumber, nationality, deleted, + userGroups.with(UserGroupFields.allFields), ) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserHandler.kt index 40227e8e9c..91f4c9b8e8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserHandler.kt @@ -38,6 +38,8 @@ internal class UserHandler( userStore: UserStore, private val userRoleHandler: UserRoleHandler, private val userRoleCollectionCleaner: UserRoleCollectionCleaner, + private val userGroupHandler: UserGroupHandler, + private val userGroupCollectionCleaner: UserGroupCollectionCleaner, ) : IdentifiableHandlerImpl(userStore) { override fun beforeObjectHandled(o: User): User { @@ -58,5 +60,7 @@ internal class UserHandler( override fun afterObjectHandled(o: User, action: HandleAction) { userRoleCollectionCleaner.deleteNotPresent(o.userRoles()) userRoleHandler.handleMany(o.userRoles()) + userGroupCollectionCleaner.deleteNotPresent(o.userGroups()) + userGroupHandler.handleMany(o.userGroups()) } } From 4c09cfcda556f8ff10b83aa00012c5dced58d18f Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:50:18 +0100 Subject: [PATCH 111/222] [ANDROSDK-1817] Add user_group json --- .../sharedTest/resources/user/user_group.json | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 core/src/sharedTest/resources/user/user_group.json diff --git a/core/src/sharedTest/resources/user/user_group.json b/core/src/sharedTest/resources/user/user_group.json new file mode 100644 index 0000000000..ca796788c7 --- /dev/null +++ b/core/src/sharedTest/resources/user/user_group.json @@ -0,0 +1,65 @@ +{ + "href": "https://play.dhis2.org/android-current/api/userGroups/Kk12LkEWtXp", + "name": "_PROGRAM_TB program", + "created": "2018-03-09T23:04:50.114", + "lastUpdated": "2024-02-07T14:00:59.251", + "translations": [ + ], + "externalAccess": false, + "publicAccess": "rw------", + "createdBy": { + "id": "GOLswS44mh8", + "code": null, + "name": "Tom Wakiki", + "displayName": "Tom Wakiki", + "username": "system" + }, + "userGroupAccesses": [ + ], + "userAccesses": [ + ], + "access": { + "manage": true, + "externalize": true, + "write": true, + "read": true, + "update": true, + "delete": true + }, + "favorites": [ + ], + "sharing": { + "owner": "GOLswS44mh8", + "external": false, + "users": { + }, + "userGroups": { + }, + "public": "rw------" + }, + "user": { + "id": "GOLswS44mh8", + "code": null, + "name": "Tom Wakiki", + "displayName": "Tom Wakiki", + "username": "system" + }, + "favorite": false, + "displayName": "_PROGRAM_TB program", + "id": "Kk12LkEWtXp", + "attributeValues": [ + ], + "users": [ + { + "id": "ERMxia28vpM", + "code": null, + "name": "Susan Barnes", + "displayName": "Susan Barnes", + "username": "android1" + } + ], + "managedGroups": [ + ], + "managedByGroups": [ + ] +} \ No newline at end of file From 3767b0cd8981807ae42bb5ce99b7ed5bb41680bc Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:50:31 +0100 Subject: [PATCH 112/222] [ANDROSDK-1817] Add user group samples --- .../core/data/user/UserGroupSamples.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 core/src/sharedTest/java/org/hisp/dhis/android/core/data/user/UserGroupSamples.java diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/user/UserGroupSamples.java b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/user/UserGroupSamples.java new file mode 100644 index 0000000000..cce8cb4753 --- /dev/null +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/user/UserGroupSamples.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.data.user; + +import static org.hisp.dhis.android.core.data.utils.FillPropertiesTestUtils.fillIdentifiableProperties; + +import org.hisp.dhis.android.core.user.UserGroup; + +public class UserGroupSamples { + + public static UserGroup getUserGroup() { + UserGroup.Builder builder = UserGroup.builder(); + + fillIdentifiableProperties(builder); + return builder + .id(1L) + .build(); + } +} \ No newline at end of file From 73eb6d118bfe62e47349c78b3089afc8cdb0b6e4 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:50:41 +0100 Subject: [PATCH 113/222] [ANDROSDK-1817] Add user group unit tests --- .../android/core/user/UserGroupShould.java | 61 +++++++++++++++++++ .../user/UserGroupPublicAccessShould.java | 59 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java create mode 100644 core/src/test/java/org/hisp/dhis/android/testapp/user/UserGroupPublicAccessShould.java diff --git a/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java b/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java new file mode 100644 index 0000000000..c1e7b7fee9 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.user; + +import static com.google.common.truth.Truth.assertThat; + +import org.hisp.dhis.android.core.common.BaseIdentifiableObject; +import org.hisp.dhis.android.core.common.BaseObjectShould; +import org.hisp.dhis.android.core.common.ObjectShould; +import org.junit.Test; + +import java.io.IOException; +import java.text.ParseException; + +public class UserGroupShould extends BaseObjectShould implements ObjectShould { + + public UserGroupShould() { + super("user/user_group.json"); + } + + @Override + @Test + public void map_from_json_string() throws IOException, ParseException { + UserGroup userGroup = objectMapper.readValue(jsonStream, UserGroup.class); + + assertThat(userGroup.lastUpdated()).isEqualTo( + BaseIdentifiableObject.DATE_FORMAT.parse("2024-02-07T14:00:59.251")); + assertThat(userGroup.created()).isEqualTo( + BaseIdentifiableObject.DATE_FORMAT.parse("2018-03-09T23:04:50.114")); + assertThat(userGroup.uid()).isEqualTo("Kk12LkEWtXp"); + assertThat(userGroup.displayName()).isEqualTo("_PROGRAM_TB program"); + assertThat(userGroup.name()).isEqualTo("_PROGRAM_TB program"); + assertThat(userGroup.code()).isEqualTo("_PROGRAM_TB program"); + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/testapp/user/UserGroupPublicAccessShould.java b/core/src/test/java/org/hisp/dhis/android/testapp/user/UserGroupPublicAccessShould.java new file mode 100644 index 0000000000..4d1765da71 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/testapp/user/UserGroupPublicAccessShould.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.testapp.user; + +import org.hisp.dhis.android.core.user.UserGroup; +import org.hisp.dhis.android.testapp.arch.BasePublicAccessShould; +import org.mockito.Mock; + +public class UserGroupPublicAccessShould extends BasePublicAccessShould { + + @Mock + private UserGroup object; + + @Override + public UserGroup object() { + return object; + } + + @Override + public void has_public_create_method() { + UserGroup.create(null); + } + + @Override + public void has_public_builder_method() { + UserGroup.builder(); + } + + @Override + public void has_public_to_builder_method() { + object().toBuilder(); + } +} \ No newline at end of file From 074b99c8f91cfa8b8e7dca6550b0a4a5c72d5d57 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 12:50:57 +0100 Subject: [PATCH 114/222] [ANDROSDK-1817] Add UserGroupStoreIntegrationShould --- .../UserGroupStoreIntegrationShould.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreIntegrationShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreIntegrationShould.kt new file mode 100644 index 0000000000..c6b66a20a8 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/UserGroupStoreIntegrationShould.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import org.hisp.dhis.android.core.data.database.IdentifiableObjectStoreAbstractIntegrationShould +import org.hisp.dhis.android.core.data.user.UserGroupSamples +import org.hisp.dhis.android.core.user.UserGroup +import org.hisp.dhis.android.core.user.UserGroupTableInfo +import org.hisp.dhis.android.core.utils.integration.mock.TestDatabaseAdapterFactory +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class UserGroupStoreIntegrationShould : IdentifiableObjectStoreAbstractIntegrationShould( + UserGroupStoreImpl(TestDatabaseAdapterFactory.get()), + UserGroupTableInfo.TABLE_INFO, + TestDatabaseAdapterFactory.get(), +) { + override fun buildObject(): UserGroup { + return UserGroupSamples.getUserGroup() + } + + override fun buildObjectToUpdate(): UserGroup { + return UserGroupSamples.getUserGroup().toBuilder() + .name("new_name") + .build() + } +} From 6fc72d030c1380e9f94aa4790342670aee9f22a2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 23 Feb 2024 13:27:42 +0100 Subject: [PATCH 115/222] [ANDROSDK-1817] Fix unit tests and Detekt --- ... => IgnoreUserGroupListColumnAdapter.java} | 10 +++++--- .../android/core/user/UserGroupTableInfo.kt | 2 +- .../core/user/internal/UserGroupFields.kt | 2 +- .../android/core/user/UserGroupShould.java | 1 - .../core/user/internal/UserHandlerShould.kt | 23 ++++++++++++++++--- 5 files changed, 29 insertions(+), 9 deletions(-) rename core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/{IgnoreUserGroupListColumnAdapter.kt => IgnoreUserGroupListColumnAdapter.java} (90%) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.java similarity index 90% rename from core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt rename to core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.java index 8678d358ea..ee9e851544 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreUserGroupListColumnAdapter.java @@ -25,8 +25,12 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal -import org.hisp.dhis.android.core.user.UserGroup +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal; -class IgnoreUserGroupListColumnAdapter : IgnoreColumnAdapter>() +import org.hisp.dhis.android.core.user.UserGroup; + +import java.util.List; + +public final class IgnoreUserGroupListColumnAdapter extends IgnoreColumnAdapter> { +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt index fa143359f0..081c084415 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroupTableInfo.kt @@ -45,4 +45,4 @@ object UserGroupTableInfo { } class Columns : IdentifiableColumns() -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt index 5d496c87e9..4ff46e01bf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserGroupFields.kt @@ -36,4 +36,4 @@ internal object UserGroupFields { val allFields: Fields = Fields.builder() .fields(fh.getIdentifiableFields()) .build() -} \ No newline at end of file +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java b/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java index c1e7b7fee9..6ffa47cc2d 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java +++ b/core/src/test/java/org/hisp/dhis/android/core/user/UserGroupShould.java @@ -56,6 +56,5 @@ public void map_from_json_string() throws IOException, ParseException { assertThat(userGroup.uid()).isEqualTo("Kk12LkEWtXp"); assertThat(userGroup.displayName()).isEqualTo("_PROGRAM_TB program"); assertThat(userGroup.name()).isEqualTo("_PROGRAM_TB program"); - assertThat(userGroup.code()).isEqualTo("_PROGRAM_TB program"); } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/user/internal/UserHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/user/internal/UserHandlerShould.kt index 6c1459d820..155149a70a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/user/internal/UserHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/user/internal/UserHandlerShould.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.UserCredentials +import org.hisp.dhis.android.core.user.UserGroup import org.hisp.dhis.android.core.user.UserRole import org.junit.Before import org.junit.Test @@ -44,8 +45,11 @@ class UserHandlerShould { private val userStore: UserStore = mock() private val userRoleHandler: UserRoleHandler = mock() private val userRoleCollectionCleaner: UserRoleCollectionCleaner = mock() + private val userGroupHandler: UserGroupHandler = mock() + private val userGroupCollectionCleaner: UserGroupCollectionCleaner = mock() private val userRoles: List = mock() + private val userGroups: List = mock() private lateinit var user: User private lateinit var userCredentials: UserCredentials @@ -55,7 +59,10 @@ class UserHandlerShould { @Before fun setUp() { - userHandler = UserHandler(userStore, userRoleHandler, userRoleCollectionCleaner) + userHandler = UserHandler( + userStore, userRoleHandler, userRoleCollectionCleaner, userGroupHandler, + userGroupCollectionCleaner, + ) userCredentials = UserCredentials.builder() .username("username") .userRoles(userRoles) @@ -63,6 +70,7 @@ class UserHandlerShould { user = User.builder() .uid("userUid") .userCredentials(userCredentials) + .userGroups(userGroups) .build() whenever(userStore.updateOrInsert(any())).thenReturn(HandleAction.Insert) @@ -71,16 +79,25 @@ class UserHandlerShould { @Test fun extend_identifiable_sync_handler_impl() { val genericHandler: IdentifiableHandlerImpl = - UserHandler(userStore, userRoleHandler, userRoleCollectionCleaner) + UserHandler( + userStore, + userRoleHandler, + userRoleCollectionCleaner, + userGroupHandler, + userGroupCollectionCleaner, + ) assertThat(genericHandler).isNotNull() } @Test - fun add_username_and_roles_from_credentials() { + fun add_username_groups_and_roles_from_credentials() { userHandler.handle(user) verify(userRoleCollectionCleaner, times(1)).deleteNotPresent(eq(userRoles)) verify(userRoleHandler, times(1)).handleMany(eq(userRoles)) + + verify(userGroupCollectionCleaner, times(1)).deleteNotPresent(eq(userGroups)) + verify(userGroupHandler, times(1)).handleMany(eq(userGroups)) } } From d91f5f7fa622000ab9bf27a6a957900e446bd4be Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 09:25:57 +0100 Subject: [PATCH 116/222] [ANDROSDK-1817] Add User module wiper --- .../org/hisp/dhis/android/core/user/internal/UserModuleWiper.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleWiper.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleWiper.kt index 33e21fe9b9..63d768eb29 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleWiper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleWiper.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.user.internal import org.hisp.dhis.android.core.user.AuthenticatedUserTableInfo import org.hisp.dhis.android.core.user.AuthorityTableInfo +import org.hisp.dhis.android.core.user.UserGroupTableInfo import org.hisp.dhis.android.core.user.UserOrganisationUnitLinkTableInfo import org.hisp.dhis.android.core.user.UserRoleTableInfo import org.hisp.dhis.android.core.user.UserTableInfo @@ -47,6 +48,7 @@ internal class UserModuleWiper( AuthenticatedUserTableInfo.TABLE_INFO, AuthorityTableInfo.TABLE_INFO, UserRoleTableInfo.TABLE_INFO, + UserGroupTableInfo.TABLE_INFO, ) } From 397aaeb786a1fc04cf6e9f0cf15627f744c12062 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 09:26:46 +0100 Subject: [PATCH 117/222] [ANDROSDK-1817] Add userGroups to user module --- .../user/UserModuleMockIntegrationShould.java | 8 ++++++++ .../hisp/dhis/android/core/user/UserModule.kt | 1 + .../core/user/internal/UserModuleImpl.kt | 5 +++++ core/src/sharedTest/resources/user/user38.json | 17 +++++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/user/UserModuleMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/user/UserModuleMockIntegrationShould.java index 37fdaaa6b8..fd62bcb638 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/user/UserModuleMockIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/user/UserModuleMockIntegrationShould.java @@ -62,6 +62,14 @@ public void allow_access_to_user_role() { assertThat(userRole.get(0).displayName()).isEqualTo("Superuser"); } + @Test + public void allow_access_to_user_group() { + List userGroups = d2.userModule().userGroups().blockingGet(); + assertThat(userGroups.get(0).uid()).isEqualTo("Kk12LkEWtXp"); + assertThat(userGroups.get(0).name()).isEqualTo("_PROGRAM_TB program"); + assertThat(userGroups.get(0).displayName()).isEqualTo("_PROGRAM_TB program"); + } + @Test public void allow_access_to_user() { User user = d2.userModule().user().blockingGet(); diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserModule.kt b/core/src/main/java/org/hisp/dhis/android/core/user/UserModule.kt index 2c07b6846a..87f11420ae 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserModule.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserModule.kt @@ -35,6 +35,7 @@ import org.hisp.dhis.android.core.user.openid.OpenIDConnectHandler interface UserModule { fun authenticatedUser(): AuthenticatedUserObjectRepository fun userRoles(): UserRoleCollectionRepository + fun userGroups(): UserGroupCollectionRepository fun authorities(): AuthorityCollectionRepository fun user(): UserObjectRepository fun accountManager(): AccountManager diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleImpl.kt index 6189a705e7..7c3cd7deab 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserModuleImpl.kt @@ -43,6 +43,7 @@ internal class UserModuleImpl( private val logInCall: LogInCall, private val authenticatedUser: AuthenticatedUserObjectRepository, private val userRoles: UserRoleCollectionRepository, + private val userGroups: UserGroupCollectionRepository, private val authorities: AuthorityCollectionRepository, private val userCredentials: UserCredentialsObjectRepository, private val user: UserObjectRepository, @@ -58,6 +59,10 @@ internal class UserModuleImpl( return userRoles } + override fun userGroups(): UserGroupCollectionRepository { + return userGroups + } + override fun authorities(): AuthorityCollectionRepository { return authorities } diff --git a/core/src/sharedTest/resources/user/user38.json b/core/src/sharedTest/resources/user/user38.json index 7cc893c83d..140ef82015 100644 --- a/core/src/sharedTest/resources/user/user38.json +++ b/core/src/sharedTest/resources/user/user38.json @@ -22,6 +22,23 @@ ] } ], + "userGroups": [ + { + "href": "https://play.dhis2.org/android-current/api/userGroups/Kk12LkEWtXp", + "name": "_PROGRAM_TB program", + "created": "2018-03-09T23:04:50.114", + "lastUpdated": "2024-02-07T14:00:59.251", + "createdBy": { + "id": "GOLswS44mh8", + "code": null, + "name": "Tom Wakiki", + "displayName": "Tom Wakiki", + "username": "system" + }, + "displayName": "_PROGRAM_TB program", + "id": "Kk12LkEWtXp" + } + ], "teiSearchOrganisationUnits": [], "organisationUnits": [ { From 7be147e19031bab7ec758d1a5faf491576bd2520 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 09:27:18 +0100 Subject: [PATCH 118/222] [ANDROSDK-1817] Add back the targetSdk version --- core/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1776d303dc..00cff339fd 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -53,6 +53,7 @@ android { defaultConfig { minSdk = libs.versions.minSdkVersion.get().toInt() + testOptions.targetSdk = libs.versions.targetSdkVersion.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true vectorDrawables.useSupportLibrary = true From 33967b6b83f909ea5bddc2ad073499f475689899 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 10:22:35 +0100 Subject: [PATCH 119/222] [ANDROSDK-1817] Use static access for Builder user group --- .../main/java/org/hisp/dhis/android/core/user/UserGroup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java index 885911dd74..c0f69c38ec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/UserGroup.java @@ -38,7 +38,7 @@ import org.hisp.dhis.android.core.common.CoreObject; @AutoValue -@JsonDeserialize(builder = AutoValue_UserGroup.Builder.class) +@JsonDeserialize(builder = $$AutoValue_UserGroup.Builder.class) public abstract class UserGroup extends BaseIdentifiableObject implements CoreObject { public static Builder builder() { From 5c66aa43d90c0102bbef2f573f75a0abd8dc1119 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:56:02 +0100 Subject: [PATCH 120/222] [ANDROSDK-1816] Add service to download versions --- .../dhis/android/core/settings/internal/SettingAppService.kt | 4 ++++ .../dhis/android/core/settings/internal/SettingService.kt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt index 31d96802de..61377a699e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt @@ -79,6 +79,10 @@ internal class SettingAppService( return settingService.latestAppVersion("$APK_DISTRIBUTION_NAMESPACE/latestVersion") } + suspend fun versions(): List { + return settingService.versions("$APK_DISTRIBUTION_NAMESPACE/versions") + } + private fun getNamespace(version: SettingsAppDataStoreVersion): String { return when (version) { SettingsAppDataStoreVersion.V1_1 -> ANDROID_APP_NAMESPACE_V1 diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt index 9c53bdf686..9a523ec623 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt @@ -69,4 +69,7 @@ internal interface SettingService { @GET suspend fun latestAppVersion(@Url url: String): LatestAppVersion + + @GET + suspend fun versions(@Url url: String): List } From eeebb36d92f6f4010061d18da68d0f9ca20527ae Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:57:06 +0100 Subject: [PATCH 121/222] [ANDROSDK-1816] Add user groups to LatestAppVersion --- .../hisp/dhis/android/core/settings/LatestAppVersion.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java index be7a2b8b8c..55e5062fa1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java @@ -39,6 +39,8 @@ import org.hisp.dhis.android.core.common.CoreObject; +import java.util.List; + @AutoValue @JsonDeserialize(builder = $$AutoValue_LatestAppVersion.Builder.class) public abstract class LatestAppVersion implements CoreObject { @@ -51,6 +53,10 @@ public abstract class LatestAppVersion implements CoreObject { @Nullable public abstract String downloadURL(); + @JsonProperty() + @Nullable + public abstract List userGroups(); + public static LatestAppVersion create(Cursor cursor) { return $AutoValue_LatestAppVersion.createFromCursor(cursor); } @@ -70,6 +76,8 @@ public abstract static class Builder { public abstract Builder downloadURL(String downloadURL); + public abstract Builder userGroups(List userGroups); + public abstract LatestAppVersion build(); } } From 6f8ced48d0c10db4919511c1515417985e200b7e Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:57:30 +0100 Subject: [PATCH 122/222] [ANDROSDK-1816] Temp: add versions call --- .../settings/internal/LatestAppVersionCall.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 3a129961ed..82f5f69385 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -30,19 +30,37 @@ package org.hisp.dhis.android.core.settings.internal import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutor import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.common.AssignedUserMode import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.LatestAppVersion +import org.hisp.dhis.android.core.user.UserModule import org.koin.core.annotation.Singleton @Singleton internal class LatestAppVersionCall( private val latestAppVersionHandler: LatestAppVersionHandler, private val settingAppService: SettingAppService, + private val userModule: UserModule, coroutineAPICallExecutor: CoroutineAPICallExecutor, ) : BaseSettingCall(coroutineAPICallExecutor) { override suspend fun tryFetch(storeError: Boolean): Result { - return coroutineAPICallExecutor.wrap(storeError = storeError) { settingAppService.latestAppVersion() } + + return coroutineAPICallExecutor.wrap(storeError = storeError) { + val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } + + val filteredVersions = settingAppService.versions().filter { version -> + version.userGroups()?.any { userGroupUid -> + userGroupUids.contains(userGroupUid) + } ?: false + } + + val highestVersion = filteredVersions.maxByOrNull { version -> + // TODO get hightest filtered version + } + + highestVersion ?: settingAppService.latestAppVersion() + } } override fun process(item: LatestAppVersion?) { From 09d4eae50b8d73a7e4c03cf73f47d594dd821693 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 27 Feb 2024 11:32:15 +0100 Subject: [PATCH 123/222] [ANDROSDK-1819] Remove index from JobObjectReport --- .../dhis/android/core/tracker/importer/internal/JobReport.kt | 1 - .../android/core/tracker/importer/JobReportSuccessShould.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReport.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReport.kt index b4bea7f608..ce480baec3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReport.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReport.kt @@ -50,7 +50,6 @@ internal data class JobValidationReport( internal data class JobObjectReport( val errorReports: List, - val index: Int, val trackerType: TrackerImporterObjectType, val uid: String, ) diff --git a/core/src/test/java/org/hisp/dhis/android/core/tracker/importer/JobReportSuccessShould.kt b/core/src/test/java/org/hisp/dhis/android/core/tracker/importer/JobReportSuccessShould.kt index 77c7a1bc35..c79368d3f6 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/tracker/importer/JobReportSuccessShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/tracker/importer/JobReportSuccessShould.kt @@ -65,7 +65,7 @@ class JobReportSuccessShould : BaseObjectShould("tracker/importer/jobreport-succ JobTypeReport( "EVENT", JobImportCount(2, 2, 2, 2, 8), - listOf(JobObjectReport(emptyList(), 0, TrackerImporterObjectType.EVENT, "UavzrupW3lZ")), + listOf(JobObjectReport(emptyList(), TrackerImporterObjectType.EVENT, "UavzrupW3lZ")), ), ) assertThat(bundleReport.typeReportMap.relationship).isEqualTo( From 25e7c3311fb0426a74b9c9e981c2316b3e9327e0 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:01:23 +0100 Subject: [PATCH 124/222] [ANDROSDK-1750] Add bypassDHIS2VersionCheck to General settings pojo --- .../org/hisp/dhis/android/core/settings/GeneralSettings.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettings.java b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettings.java index 2acd5b1f5a..4891b1992b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettings.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettings.java @@ -116,6 +116,9 @@ public String numberSmsConfirmation() { @ColumnAdapter(StringListColumnAdapter.class) public abstract List experimentalFeatures(); + @Nullable + public abstract Boolean bypassDHIS2VersionCheck(); + public static GeneralSettings create(Cursor cursor) { return $AutoValue_GeneralSettings.createFromCursor(cursor); } @@ -158,6 +161,8 @@ public abstract static class Builder { public abstract Builder experimentalFeatures(List experimentalFeatures); + public abstract Builder bypassDHIS2VersionCheck(Boolean bypassDHIS2VersionCheck); + public abstract GeneralSettings build(); } } \ No newline at end of file From 783330d61e53d94deed86cff1f959204de7a3806 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:01:43 +0100 Subject: [PATCH 125/222] [ANDROSDK-1750] Rename .java to .kt --- .../{GeneralSettingTableInfo.java => GeneralSettingTableInfo.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/src/main/java/org/hisp/dhis/android/core/settings/{GeneralSettingTableInfo.java => GeneralSettingTableInfo.kt} (100%) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.java b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt similarity index 100% rename from core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.java rename to core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt From 69b3f084fa1eeb617aa018aab8da40fb98157cae Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:01:44 +0100 Subject: [PATCH 126/222] [ANDROSDK-1750] Add bypassDHIS2VersionCheck to table info and move to kotlin --- .../core/settings/GeneralSettingTableInfo.kt | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt index 0baa3ce83d..ec664a40a2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt @@ -25,57 +25,53 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.settings -package org.hisp.dhis.android.core.settings; +import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo +import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper +import org.hisp.dhis.android.core.common.CoreColumns -import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo; -import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper; -import org.hisp.dhis.android.core.common.CoreColumns; - -public final class GeneralSettingTableInfo { - - private GeneralSettingTableInfo() { - } - - public static final TableInfo TABLE_INFO = new TableInfo() { - - @Override - public String name() { - return "GeneralSetting"; +object GeneralSettingTableInfo { + val TABLE_INFO: TableInfo = object : TableInfo() { + override fun name(): String { + return "GeneralSetting" } - @Override - public CoreColumns columns() { - return new Columns(); + override fun columns(): CoreColumns { + return Columns() } - }; + } - public static class Columns extends CoreColumns { - public static final String ENCRYPT_DB = "encryptDB"; - public static final String LAST_UPDATED = "lastUpdated"; - public static final String RESERVED_VALUES = "reservedValues"; - public static final String SMS_GATEWAY = "smsGateway"; - public static final String SMS_RESULT_SENDER = "smsResultSender"; - public static final String MATOMO_ID = "matomoID"; - public static final String MATOMO_URL = "matomoURL"; - public static final String ALLOW_SCREEN_CAPTURE = "allowScreenCapture"; - public static final String MESSAGE_OF_THE_DAY = "messageOfTheDay"; - public static final String EXPERIMENTAL_FEATURES = "experimentalFeatures"; + class Columns : CoreColumns() { + override fun all(): Array { + return CollectionsHelper.appendInNewArray( + super.all(), + ENCRYPT_DB, + LAST_UPDATED, + RESERVED_VALUES, + SMS_GATEWAY, + SMS_RESULT_SENDER, + MATOMO_ID, + MATOMO_URL, + ALLOW_SCREEN_CAPTURE, + MESSAGE_OF_THE_DAY, + EXPERIMENTAL_FEATURES, + BYPASS_DHIS2_VERSION_CHECK, + ) + } - @Override - public String[] all() { - return CollectionsHelper.appendInNewArray(super.all(), - ENCRYPT_DB, - LAST_UPDATED, - RESERVED_VALUES, - SMS_GATEWAY, - SMS_RESULT_SENDER, - MATOMO_ID, - MATOMO_URL, - ALLOW_SCREEN_CAPTURE, - MESSAGE_OF_THE_DAY, - EXPERIMENTAL_FEATURES - ); + companion object { + const val ENCRYPT_DB = "encryptDB" + const val LAST_UPDATED = "lastUpdated" + const val RESERVED_VALUES = "reservedValues" + const val SMS_GATEWAY = "smsGateway" + const val SMS_RESULT_SENDER = "smsResultSender" + const val MATOMO_ID = "matomoID" + const val MATOMO_URL = "matomoURL" + const val ALLOW_SCREEN_CAPTURE = "allowScreenCapture" + const val MESSAGE_OF_THE_DAY = "messageOfTheDay" + const val EXPERIMENTAL_FEATURES = "experimentalFeatures" + const val BYPASS_DHIS2_VERSION_CHECK = "bypassDHIS2VersionCheck" } } } \ No newline at end of file From 77b5c8be91620bdb0fad44a84bcdbaabfcb8cb5a Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:01:55 +0100 Subject: [PATCH 127/222] [ANDROSDK-1750] Add bypassDHIS2VersionCheck to store --- .../android/core/settings/internal/GeneralSettingStoreImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingStoreImpl.kt index 0328bd1ac1..4d230d9ff1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingStoreImpl.kt @@ -64,6 +64,7 @@ internal class GeneralSettingStoreImpl( w.bind(8, o.allowScreenCapture()) w.bind(9, o.messageOfTheDay()) w.bind(10, StringListColumnAdapter.serialize(o.experimentalFeatures())) + w.bind(11, o.bypassDHIS2VersionCheck()) } private val WHERE_UPDATE_BINDER = WhereStatementBinder { From acf537ed2ee519d5616dad5e2fef4651779edcb9 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:02:14 +0100 Subject: [PATCH 128/222] [ANDROSDK-1750] Add bypassDHIS2VersionCheck to migration and snapshot --- core/src/main/assets/migrations/163.sql | 3 +++ core/src/main/assets/snapshots/snapshot.sql | 2 +- .../core/arch/db/access/internal/BaseDatabaseOpenHelper.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 core/src/main/assets/migrations/163.sql diff --git a/core/src/main/assets/migrations/163.sql b/core/src/main/assets/migrations/163.sql new file mode 100644 index 0000000000..33e44bf8fd --- /dev/null +++ b/core/src/main/assets/migrations/163.sql @@ -0,0 +1,3 @@ +# Add bypassDHIS2VersionCheck to General Settings (ANDROSDK-1750) + +ALTER TABLE GeneralSetting ADD COLUMN bypassDHIS2VersionCheck INTEGER; \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index a4c4d17279..93719f5894 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -79,7 +79,7 @@ CREATE TABLE SectionGreyedFieldsLink (_id INTEGER PRIMARY KEY AUTOINCREMENT, sec CREATE TABLE AuthenticatedUser (_id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT NOT NULL UNIQUE, hash TEXT, FOREIGN KEY (user) REFERENCES User (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE UNIQUE INDEX event_data_element ON TrackedEntityDataValue(event, dataElement); CREATE UNIQUE INDEX tracked_entity_instance_attribute ON TrackedEntityAttributeValue(trackedEntityInstance, trackedEntityAttribute); -CREATE TABLE GeneralSetting (_id INTEGER PRIMARY KEY AUTOINCREMENT, encryptDB INTEGER, lastUpdated TEXT, reservedValues INTEGER, smsGateway TEXT, smsResultSender TEXT, matomoID INTEGER, matomoURL TEXT, allowScreenCapture INTEGER, messageOfTheDay TEXT, experimentalFeatures TEXT); +CREATE TABLE GeneralSetting (_id INTEGER PRIMARY KEY AUTOINCREMENT, encryptDB INTEGER, lastUpdated TEXT, reservedValues INTEGER, smsGateway TEXT, smsResultSender TEXT, matomoID INTEGER, matomoURL TEXT, allowScreenCapture INTEGER, messageOfTheDay TEXT, experimentalFeatures TEXT, bypassDHIS2VersionCheck INTEGER); CREATE TABLE DataSetSetting (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, name TEXT, lastUpdated TEXT, periodDSDownload INTEGER, periodDSDBTrimming INTEGER, FOREIGN KEY (uid) REFERENCES DataSet (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE ProgramSetting (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, name TEXT, lastUpdated TEXT, teiDownload INTEGER, teiDBTrimming INTEGER, eventsDownload INTEGER, eventsDBTrimming INTEGER, updateDownload TEXT, updateDBTrimming TEXT, settingDownload TEXT, settingDBTrimming TEXT, enrollmentDownload TEXT, enrollmentDBTrimming TEXT, eventDateDownload TEXT, eventDateDBTrimming TEXT, enrollmentDateDownload TEXT, enrollmentDateDBTrimming TEXT, FOREIGN KEY (uid) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE SynchronizationSetting (_id INTEGER PRIMARY KEY AUTOINCREMENT, dataSync TEXT, metadataSync TEXT, trackerImporterVersion TEXT, trackerExporterVersion TEXT, fileMaxLengthBytes INTEGER); diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt index bcae0fe711..2624a312ae 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/BaseDatabaseOpenHelper.kt @@ -59,6 +59,6 @@ internal class BaseDatabaseOpenHelper(context: Context, targetVersion: Int) { } companion object { - const val VERSION = 162 + const val VERSION = 163 } } From 47c797022f11b3be247767560a373bb884e53b8c Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:02:34 +0100 Subject: [PATCH 129/222] [ANDROSDK-1750] Update general settings samples and json --- .../dhis/android/core/data/settings/GeneralSettingsSamples.kt | 1 + .../src/sharedTest/resources/settings/general_settings_v2.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/GeneralSettingsSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/GeneralSettingsSamples.kt index 802de20ade..3737f290cf 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/GeneralSettingsSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/settings/GeneralSettingsSamples.kt @@ -44,6 +44,7 @@ object GeneralSettingsSamples { .allowScreenCapture(true) .messageOfTheDay("Message of the day") .experimentalFeatures(listOf("newFormLayout")) + .bypassDHIS2VersionCheck(true) .build() } } diff --git a/core/src/sharedTest/resources/settings/general_settings_v2.json b/core/src/sharedTest/resources/settings/general_settings_v2.json index d66244b75b..b481a5076b 100644 --- a/core/src/sharedTest/resources/settings/general_settings_v2.json +++ b/core/src/sharedTest/resources/settings/general_settings_v2.json @@ -9,5 +9,6 @@ "messageOfTheDay": "Message of the day", "experimentalFeatures": [ "newFormLayout" - ] + ], + "bypassDHIS2VersionCheck": true } \ No newline at end of file From e38f9f725760fd2e595fa5c988f0a933f9313b96 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:02:47 +0100 Subject: [PATCH 130/222] [ANDROSDK-1750] Update unit test --- .../hisp/dhis/android/core/settings/GeneralSettingsV2Should.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/GeneralSettingsV2Should.java b/core/src/test/java/org/hisp/dhis/android/core/settings/GeneralSettingsV2Should.java index 6eaad1f740..8b7bdcf43b 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/GeneralSettingsV2Should.java +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/GeneralSettingsV2Should.java @@ -63,5 +63,6 @@ public void map_from_json_string() throws IOException, ParseException { assertThat(generalSettings.messageOfTheDay()).isEqualTo("Message of the day"); assertThat(generalSettings.experimentalFeatures().size()).isEqualTo(1); assertThat(generalSettings.experimentalFeatures().get(0)).isEqualTo("newFormLayout"); + assertThat(generalSettings.bypassDHIS2VersionCheck()).isTrue(); } } From f865e59919b7899845a5c63daf10afbf31d6d5e2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 27 Feb 2024 12:02:54 +0100 Subject: [PATCH 131/222] [ANDROSDK-1750] Update integration test --- .../GeneralSettingsObjectRepositoryMockIntegrationShould.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/GeneralSettingsObjectRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/GeneralSettingsObjectRepositoryMockIntegrationShould.kt index 578451e4f4..effc83a043 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/GeneralSettingsObjectRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/GeneralSettingsObjectRepositoryMockIntegrationShould.kt @@ -41,6 +41,7 @@ class GeneralSettingsObjectRepositoryMockIntegrationShould : BaseMockIntegration fun find_android_setting() { val generalSettings = d2.settingModule().generalSetting().blockingGet() assertThat(generalSettings!!.dataSync()).isEqualTo(DataSyncPeriod.EVERY_24_HOURS) + assertThat(generalSettings!!.bypassDHIS2VersionCheck()).isTrue() } @Test From 57ae283095da0856a73684ee1f1837958d2c3974 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 21 Feb 2024 13:11:39 +0100 Subject: [PATCH 132/222] [ANDROSDK-1809] Implement tracker linelist evaluators --- ...ckerLineListRepositoryIntegrationShould.kt | 1 + .../trackerlinelist/TrackerLineListModel.kt | 12 +-- .../TrackerLineListResponse.kt | 2 +- .../internal/TrackerLineListParams.kt | 6 ++ .../internal/TrackerLineListService.kt | 95 +++++++++++++------ .../internal/evaluator/BaseDateEvaluator.kt | 86 +++++++++++++++++ .../evaluator/EnrollmentDateEvaluator.kt | 46 +++++++++ .../internal/evaluator/EventDateEvaluator.kt | 54 +++++++++++ .../evaluator/EventStatusEvaluator.kt | 58 +++++++++++ .../evaluator/IncidentDateEvaluator.kt | 46 +++++++++ .../evaluator/LastUpdatedEvaluator.kt | 52 ++++++++++ .../evaluator/OrganisationUnitEvaluator.kt | 63 ++++++++++++ .../evaluator/ProgramAttributeEvaluator.kt | 32 +++---- .../evaluator/ProgramDataElementEvaluator.kt | 73 ++++++++++++++ .../evaluator/ProgramStatusEvaluator.kt | 50 ++++++++++ .../evaluator/ScheduledDateEvaluator.kt | 54 +++++++++++ .../evaluator/TrackerLineListEvaluator.kt | 26 ++++- .../TrackerLineListEvaluatorMapper.kt | 9 ++ .../evaluator/TrackerLineListSQLLabel.kt | 1 + 19 files changed, 705 insertions(+), 61 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index b076903c57..a0280ca5e5 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -37,6 +37,7 @@ class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDi fun evaluate_program_attributes() { val result = d2.analyticsModule().trackerLineList() .withEventOutput("IpHINAT79UW", "dBwrot7S420") + .withColumn(TrackerLineListItem.OrganisationUnitItem(filters = emptyList())) .withColumn( TrackerLineListItem.ProgramAttribute( uid = "cejWyOfXge6", diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 1cc1237f83..1bc64aded5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -38,12 +38,12 @@ sealed class TrackerLineListItem(val id: String) { class OrganisationUnitItem(val filters: List) : TrackerLineListItem(Label.OrganisationUnit) - sealed class DateItem(id: String) : TrackerLineListItem(id) { - data class LastUpdated(val filters: List) : DateItem(Label.LastUpdated) - data class IncidentDate(val filters: List) : DateItem(Label.IncidentDate) - data class EnrollmentDate(val filters: List) : DateItem(Label.EnrollmentDate) - data class ScheduledDate(val filters: List) : DateItem(Label.ScheduledDate) - data class EventDate(val filters: List) : DateItem(Label.EventDate) + sealed class DateItem(id: String, val filters: List) : TrackerLineListItem(id) { + class LastUpdated(filters: List) : DateItem(Label.LastUpdated, filters) + class IncidentDate(filters: List) : DateItem(Label.IncidentDate, filters) + class EnrollmentDate(filters: List) : DateItem(Label.EnrollmentDate, filters) + class ScheduledDate(filters: List) : DateItem(Label.ScheduledDate, filters) + class EventDate(filters: List) : DateItem(Label.EventDate, filters) } data class ProgramIndicator(val uid: String, val filters: List) : TrackerLineListItem(uid) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt index f6338d4971..eaf01f2705 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt @@ -34,7 +34,7 @@ data class TrackerLineListResponse( val metadata: Map, val headers: List, val filters: List, - val rows: List, + val rows: List>, ) data class TrackerLineListValue( diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index a9cab82004..8347261e54 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -38,6 +38,8 @@ internal data class TrackerLineListParams( val columns: List, val filters: List, ) { + val allItems = columns + filters + operator fun plus(other: TrackerLineListParams): TrackerLineListParams { return copy( outputType = other.outputType ?: outputType, @@ -63,4 +65,8 @@ internal data class TrackerLineListParams( filters = filters.filterNot { it.id == item.id } + item, ) } + + fun hasOrgunit(): Boolean { + return (columns + filters).any { it is TrackerLineListItem.OrganisationUnitItem} + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index fa3afcc21c..f549e02809 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -36,13 +36,16 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.OrgunitAlias import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.event.EventTableInfo +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitTableInfo import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.TrackerVisualizationCollectionRepository import org.koin.core.annotation.Singleton +import java.lang.RuntimeException @Singleton internal class TrackerLineListService( @@ -61,7 +64,7 @@ internal class TrackerLineListService( val sqlClause = when (evaluatedParams.outputType!!) { TrackerLineListOutputType.EVENT -> getEventSqlClause(evaluatedParams, metadata) - TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause() + TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(evaluatedParams, metadata) } val cursor = databaseAdapter.rawQuery(sqlClause) @@ -77,6 +80,8 @@ internal class TrackerLineListService( ) } catch (e: AnalyticsException) { Result.Failure(e) + } catch (e: RuntimeException) { + Result.Failure(AnalyticsException.SQLException(e.message ?: "")) } } @@ -100,45 +105,79 @@ internal class TrackerLineListService( private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { return "SELECT " + - "${getEventSelectColumns(params, metadata)} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + - "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, metadata)} " + "${getEventSelectColumns(params, metadata)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + if (params.hasOrgunit()) { + "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ORGANISATION_UNIT} = " + + "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " + } else { + "" + } + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, metadata)} " } - private fun getEnrollmentSqlClause(): String { - return TODO() + private fun getEnrollmentSqlClause(params: TrackerLineListParams, metadata: Map): String { + return "SELECT " + + "${getEnrollmentSelectColumns(params, metadata)} " + + "FROM ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + if (params.hasOrgunit()) { + "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + + "ON $EnrollmentAlias.${EnrollmentTableInfo.Columns.ORGANISATION_UNIT} = " + + "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " + } else { + "" + } + + "WHERE " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "${getEnrollmentWhereClause(params, metadata)} " + } + + private fun getEventSelectColumns(params: TrackerLineListParams, metadata: Map): String { + return params.allItems.joinToString(", ") { + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEvent()}) ${it.id}" + } + } + + private fun getEventWhereClause(params: TrackerLineListParams, metadata: Map): String { + return params.allItems.joinToString(" AND ") { + TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEvent() + } + } + + private fun getEnrollmentSelectColumns(params: TrackerLineListParams, metadata: Map): String { + return params.allItems.joinToString(", ") { + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEnrollment()}) ${it.id}" + } } - private fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List { - val values: MutableList = mutableListOf() + private fun getEnrollmentWhereClause(params: TrackerLineListParams, metadata: Map): String { + return params.allItems.joinToString(" AND ") { + TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEnrollment() + } + } + + private fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List> { + val values: MutableList> = mutableListOf() cursor.use { c -> if (c.count > 0) { c.moveToFirst() do { - params.columns.forEachIndexed { index, item -> - values.add(TrackerLineListValue(item.id, cursor.getString(index))) + val row: MutableList = mutableListOf() + params.columns.forEach { item -> + val columnIndex = cursor.getColumnIndex(item.id) + row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) } + values.add(row) } while (c.moveToNext()) } } return values } - - private fun getEventSelectColumns(params: TrackerLineListParams, metadata: Map): String { - return params.columns.joinToString(", ") { - "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEvent()}) ${it.id}" - } - } - - private fun getEventWhereClause(params: TrackerLineListParams, metadata: Map): String { - return (params.columns + params.filters).joinToString(" AND ") { - TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEvent() - } - } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt new file mode 100644 index 0000000000..ec8385a7f8 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.period.internal.CalendarProviderFactory +import org.hisp.dhis.android.core.period.internal.ParentPeriodGeneratorImpl +import org.hisp.dhis.android.core.period.internal.PeriodParser +import java.util.Date + +internal abstract class BaseDateEvaluator( + private val item: TrackerLineListItem.DateItem, +) : TrackerLineListEvaluator() { + + private val parentPeriodGenerator = ParentPeriodGeneratorImpl.create(CalendarProviderFactory.calendarProvider) + private val periodParser = PeriodParser(CalendarProviderFactory.calendarProvider) + + fun getDateWhereClause(): String { + return if (item.filters.isEmpty()) { + "1" + } else { + return item.filters.joinToString(" AND ") { getFilterWhereClause(it) } + } + } + private fun getFilterWhereClause(filter: DateFilter): String { + return when (filter) { + is DateFilter.Absolute -> { + val periodType = PeriodType.periodTypeFromPeriodId(filter.uid) + val date = periodParser.parse(filter.uid) + val period = parentPeriodGenerator.generatePeriod(periodType, date, 0)!! + + betweenDates(period.startDate()!!, period.endDate()!!) + } + + is DateFilter.Relative -> { + val periods = parentPeriodGenerator.generateRelativePeriods(filter.relative) + + betweenDates(periods.first().startDate()!!, periods.last().endDate()!!) + } + + is DateFilter.Range -> { + betweenDates(filter.startDate, filter.endDate) + } + } + } + + private fun betweenDates(startDate: Date, endDate: Date): String { + return betweenDates( + startDate = DateUtils.DATE_FORMAT.format(startDate), + endDate = DateUtils.DATE_FORMAT.format(endDate) + ) + } + + private fun betweenDates(startDate: String, endDate: String): String { + return "julianday(${item.id}) > julianday('$startDate') AND julianday(${item.id}) < julianday('$endDate')" + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt new file mode 100644 index 0000000000..f99bce9834 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo + +internal class EnrollmentDateEvaluator( + item: TrackerLineListItem.DateItem.EnrollmentDate, +) : BaseDateEvaluator(item) { + + override fun getCommonSelectSQL(): String { + return "$EnrollmentAlias.${EnrollmentTableInfo.Columns.ENROLLMENT_DATE}" + } + + override fun getCommonWhereSQL(): String { + return getDateWhereClause() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt new file mode 100644 index 0000000000..d8c0040a3c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.event.EventTableInfo + +internal class EventDateEvaluator( + item: TrackerLineListItem.DateItem.EventDate, +) : BaseDateEvaluator(item) { + + override fun getSelectSQLForEvent(): String { + return "$EventAlias.${EventTableInfo.Columns.EVENT_DATE}" + } + + override fun getWhereSQLForEnrollment(): String { + return TODO() + } + + override fun getWhereSQLForEvent(): String { + return getDateWhereClause() + } + + override fun getSelectSQLForEnrollment(): String { + return TODO() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt new file mode 100644 index 0000000000..370c9a167d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.event.EventTableInfo + +internal class EventStatusEvaluator( + private val item: TrackerLineListItem.EventStatusItem, +) : TrackerLineListEvaluator() { + + override fun getSelectSQLForEvent(): String { + return "$EventAlias.${EventTableInfo.Columns.STATUS}" + } + + override fun getSelectSQLForEnrollment(): String { + return TODO() + } + + override fun getWhereSQLForEvent(): String { + return if (item.filters.isEmpty()) { + "1" + } else { + "${item.id} IN (${item.filters.joinToString(", ") { "'${it.name}'" }})" + } + } + + override fun getWhereSQLForEnrollment(): String { + return TODO() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt new file mode 100644 index 0000000000..43e687c07c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo + +internal class IncidentDateEvaluator( + item: TrackerLineListItem.DateItem.IncidentDate, +) : BaseDateEvaluator(item) { + + override fun getCommonSelectSQL(): String { + return "$EnrollmentAlias.${EnrollmentTableInfo.Columns.INCIDENT_DATE}" + } + + override fun getCommonWhereSQL(): String { + return getDateWhereClause() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt new file mode 100644 index 0000000000..8c8726879c --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo +import org.hisp.dhis.android.core.event.EventTableInfo + +internal class LastUpdatedEvaluator( + item: TrackerLineListItem.DateItem.LastUpdated, +) : BaseDateEvaluator(item) { + + override fun getSelectSQLForEvent(): String { + return "$EventAlias.${EventTableInfo.Columns.LAST_UPDATED}" + } + + override fun getSelectSQLForEnrollment(): String { + return "$EnrollmentAlias.${EnrollmentTableInfo.Columns.LAST_UPDATED}" + } + + override fun getCommonWhereSQL(): String { + return getDateWhereClause() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt new file mode 100644 index 0000000000..79dc70dae9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.OrganisationUnitFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.OrgunitAlias +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitTableInfo + +internal class OrganisationUnitEvaluator( + private val item: TrackerLineListItem.OrganisationUnitItem, +) : TrackerLineListEvaluator() { + override fun getCommonSelectSQL(): String { + return "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.DISPLAY_NAME}" + } + + override fun getCommonWhereSQL(): String { + return if (item.filters.isEmpty()) { + "1" + } else { + return item.filters.joinToString(" AND ") { getFilterWhereClause(it) } + } + } + + private fun getFilterWhereClause(filter: OrganisationUnitFilter): String { + return when (filter) { + is OrganisationUnitFilter.Absolute -> + "${OrganisationUnitTableInfo.Columns.PATH} LIKE '%${filter.uid}%'" + + is OrganisationUnitFilter.Relative -> TODO() + + is OrganisationUnitFilter.Level -> TODO() + + is OrganisationUnitFilter.Group -> TODO() + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt index 5cbb453613..2af9141663 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt @@ -34,42 +34,32 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast internal class ProgramAttributeEvaluator( private val item: TrackerLineListItem.ProgramAttribute, private val metadata: Map, -) : TrackerLineListEvaluator { - override fun getSelectSQLForEvent(): String { - val column = getColumnValueCast( - column = TrackedEntityAttributeValueTableInfo.Columns.VALUE, - valueType = getAttribute().valueType(), - ) - return "SELECT $column " + +) : TrackerLineListEvaluator() { + override fun getCommonSelectSQL(): String { + return "SELECT ${getColumnSql()} " + "FROM ${TrackedEntityAttributeValueTableInfo.TABLE_INFO.name()} " + "WHERE ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_INSTANCE} = " + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.TRACKED_ENTITY_INSTANCE} " + "AND ${TrackedEntityAttributeValueTableInfo.Columns.TRACKED_ENTITY_ATTRIBUTE} = '${item.id}'" } - override fun getWhereSQLForEvent(): String { + override fun getCommonWhereSQL(): String { return DataFilterHelper.getWhereClause(item.id, item.filters) } - override fun getSelectSQLForEnrollment(): String { - TODO("Not yet implemented") - } + private fun getColumnSql(): String { + val attributeMetadata = metadata[item.id] ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.id) + val attribute = ((attributeMetadata) as MetadataItem.TrackedEntityAttributeItem).item - override fun getWhereSQLForEnrollment(): String { - TODO("Not yet implemented") - } - - private fun getAttribute(): TrackedEntityAttribute { - return ( - (metadata[item.id] ?: throw AnalyticsException.InvalidTrackedEntityAttribute(item.id)) as - MetadataItem.TrackedEntityAttributeItem - ).item + return getColumnValueCast( + column = TrackedEntityAttributeValueTableInfo.Columns.VALUE, + valueType = attribute.valueType(), + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt new file mode 100644 index 0000000000..5f395a5a27 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.event.EventTableInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo +import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast + +internal class ProgramDataElementEvaluator( + private val item: TrackerLineListItem.ProgramDataElement, + private val metadata: Map, +) : TrackerLineListEvaluator() { + override fun getSelectSQLForEvent(): String { + return "SELECT ${getColumnSql()} " + + "FROM ${TrackedEntityDataValueTableInfo.TABLE_INFO.name()} " + + "WHERE ${TrackedEntityDataValueTableInfo.Columns.EVENT} = " + + "$EventAlias.${EventTableInfo.Columns.UID} " + + "AND ${TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT} = '${item.uid}'" + } + + override fun getSelectSQLForEnrollment(): String { + return TODO() + } + + override fun getWhereSQLForEvent(): String { + return DataFilterHelper.getWhereClause(item.id, item.filters) + } + + override fun getWhereSQLForEnrollment(): String { + return TODO() + } + + private fun getColumnSql(): String { + val dataElementMetadata = metadata[item.id] ?: throw AnalyticsException.InvalidDataElement(item.id) + val dataElement = ((dataElementMetadata) as MetadataItem.DataElementItem).item + + return getColumnValueCast( + column = TrackedEntityDataValueTableInfo.Columns.VALUE, + valueType = dataElement.valueType(), + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt new file mode 100644 index 0000000000..c43a99e86a --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo + +internal class ProgramStatusEvaluator( + private val item: TrackerLineListItem.ProgramStatusItem, +) : TrackerLineListEvaluator() { + + override fun getCommonSelectSQL(): String { + return "$EnrollmentAlias.${EnrollmentTableInfo.Columns.STATUS}" + } + + override fun getCommonWhereSQL(): String { + return if (item.filters.isEmpty()) { + "1" + } else { + "${item.id} IN (${item.filters.joinToString(", ") { "'${it.name}'" }})" + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt new file mode 100644 index 0000000000..94bfdece61 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.event.EventTableInfo + +internal class ScheduledDateEvaluator( + item: TrackerLineListItem.DateItem.ScheduledDate, +) : BaseDateEvaluator(item) { + + override fun getSelectSQLForEvent(): String { + return "$EventAlias.${EventTableInfo.Columns.DUE_DATE}" + } + + override fun getWhereSQLForEnrollment(): String { + return TODO() + } + + override fun getWhereSQLForEvent(): String { + return getDateWhereClause() + } + + override fun getSelectSQLForEnrollment(): String { + return TODO() + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt index fafd5397f2..5cb65da779 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt @@ -28,9 +28,25 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator -internal interface TrackerLineListEvaluator { - fun getSelectSQLForEvent(): String - fun getWhereSQLForEvent(): String - fun getSelectSQLForEnrollment(): String - fun getWhereSQLForEnrollment(): String +internal open class TrackerLineListEvaluator { + open fun getSelectSQLForEvent(): String { + return getCommonSelectSQL() + } + open fun getWhereSQLForEvent(): String { + return getCommonWhereSQL() + } + open fun getSelectSQLForEnrollment(): String { + return getCommonSelectSQL() + } + open fun getWhereSQLForEnrollment(): String { + return getCommonWhereSQL() + } + + protected open fun getCommonSelectSQL(): String { + throw RuntimeException("Not implemented") + } + + protected open fun getCommonWhereSQL(): String { + throw RuntimeException("Not implemented") + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt index 172eb2cba1..23cd009060 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -35,6 +35,15 @@ internal object TrackerLineListEvaluatorMapper { fun getEvaluator(item: TrackerLineListItem, metadata: Map): TrackerLineListEvaluator { return when (item) { is TrackerLineListItem.ProgramAttribute -> ProgramAttributeEvaluator(item, metadata) + is TrackerLineListItem.ProgramDataElement -> ProgramDataElementEvaluator(item, metadata) + is TrackerLineListItem.OrganisationUnitItem -> OrganisationUnitEvaluator(item) + is TrackerLineListItem.DateItem.LastUpdated -> LastUpdatedEvaluator(item) + is TrackerLineListItem.DateItem.IncidentDate -> IncidentDateEvaluator(item) + is TrackerLineListItem.DateItem.EnrollmentDate -> EnrollmentDateEvaluator(item) + is TrackerLineListItem.DateItem.ScheduledDate -> ScheduledDateEvaluator(item) + is TrackerLineListItem.DateItem.EventDate -> EventDateEvaluator(item) + is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) + is TrackerLineListItem.EventStatusItem -> EventStatusEvaluator(item) else -> TODO() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt index 322aec0ddf..43cce6e4ba 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt @@ -31,4 +31,5 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator object TrackerLineListSQLLabel { const val EventAlias = "ev" const val EnrollmentAlias = "en" + const val OrgunitAlias = "ou" } From 570ed31dbeba51c441f4c073491646c26945b56d Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 22 Feb 2024 09:24:58 +0100 Subject: [PATCH 133/222] [ANDROSDK-1809] Add all evaluators --- .../internal/evaluator/EventDateEvaluator.kt | 5 ++- .../evaluator/EventStatusEvaluator.kt | 5 ++- .../evaluator/NotSupportedEvaluator.kt | 40 +++++++++++++++++++ .../evaluator/ScheduledDateEvaluator.kt | 5 ++- .../TrackerLineListEvaluatorMapper.kt | 11 +++-- 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt index d8c0040a3c..b761b4c43e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator +import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias import org.hisp.dhis.android.core.event.EventTableInfo @@ -41,7 +42,7 @@ internal class EventDateEvaluator( } override fun getWhereSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("EventDate is not supported in ENROLLMENT output type") } override fun getWhereSQLForEvent(): String { @@ -49,6 +50,6 @@ internal class EventDateEvaluator( } override fun getSelectSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("EventDate is not supported in ENROLLMENT output type") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt index 370c9a167d..6a60b3b5d0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator +import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias import org.hisp.dhis.android.core.event.EventTableInfo @@ -41,7 +42,7 @@ internal class EventStatusEvaluator( } override fun getSelectSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("EventStatus is not supported in ENROLLMENT output type") } override fun getWhereSQLForEvent(): String { @@ -53,6 +54,6 @@ internal class EventStatusEvaluator( } override fun getWhereSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("EventStatus is not supported in ENROLLMENT output type") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt new file mode 100644 index 0000000000..8c5b1081b5 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +internal class NotSupportedEvaluator : TrackerLineListEvaluator() { + + override fun getCommonSelectSQL(): String { + return "Not supported" + } + + override fun getCommonWhereSQL(): String { + return "1" + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt index 94bfdece61..e66c789bee 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator +import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias import org.hisp.dhis.android.core.event.EventTableInfo @@ -41,7 +42,7 @@ internal class ScheduledDateEvaluator( } override fun getWhereSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("ScheduledDate is not supported in ENROLLMENT output type") } override fun getWhereSQLForEvent(): String { @@ -49,6 +50,6 @@ internal class ScheduledDateEvaluator( } override fun getSelectSQLForEnrollment(): String { - return TODO() + throw AnalyticsException.InvalidArguments("ScheduledDate is not supported in ENROLLMENT output type") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt index 23cd009060..de7bdea8cb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -36,15 +36,20 @@ internal object TrackerLineListEvaluatorMapper { return when (item) { is TrackerLineListItem.ProgramAttribute -> ProgramAttributeEvaluator(item, metadata) is TrackerLineListItem.ProgramDataElement -> ProgramDataElementEvaluator(item, metadata) + is TrackerLineListItem.ProgramIndicator -> TODO() is TrackerLineListItem.OrganisationUnitItem -> OrganisationUnitEvaluator(item) + + is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) + is TrackerLineListItem.EventStatusItem -> EventStatusEvaluator(item) + is TrackerLineListItem.DateItem.LastUpdated -> LastUpdatedEvaluator(item) is TrackerLineListItem.DateItem.IncidentDate -> IncidentDateEvaluator(item) is TrackerLineListItem.DateItem.EnrollmentDate -> EnrollmentDateEvaluator(item) is TrackerLineListItem.DateItem.ScheduledDate -> ScheduledDateEvaluator(item) is TrackerLineListItem.DateItem.EventDate -> EventDateEvaluator(item) - is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) - is TrackerLineListItem.EventStatusItem -> EventStatusEvaluator(item) - else -> TODO() + + is TrackerLineListItem.CreatedBy -> NotSupportedEvaluator() + is TrackerLineListItem.LastUpdatedBy -> NotSupportedEvaluator() } } } From 6fb1cd890ea77e9fe01cd9984bec1b62bd71695c Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 23 Feb 2024 10:36:15 +0100 Subject: [PATCH 134/222] [ANDROSDK-1809] Add programIndicator evaluator --- .../internal/DataFilterHelper.kt | 6 +- .../internal/TrackerLineListContext.kt | 37 ++++++ .../internal/TrackerLineListService.kt | 34 ++--- .../TrackerLineListServiceMetadataHelper.kt | 12 ++ .../evaluator/ProgramIndicatorEvaluator.kt | 121 ++++++++++++++++++ .../TrackerLineListEvaluatorMapper.kt | 11 +- .../evaluator/TrackerLineListSQLLabel.kt | 8 +- 7 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListContext.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt index 8c7dffe291..4637b2155f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt @@ -46,9 +46,9 @@ internal object DataFilterHelper { is DataFilter.EqualToIgnoreCase -> "= '${filter.value}' COLLATE NOCASE" is DataFilter.NotEqualToIgnoreCase -> "!= '${filter.value}' COLLATE NOCASE" is DataFilter.GreaterThan -> "> ${filter.value}" - is DataFilter.GreaterThanOrEqualTo -> ">= '${filter.value}'" - is DataFilter.LowerThan -> "< '${filter.value}'" - is DataFilter.LowerThanOrEqualTo -> "<= '${filter.value}'" + is DataFilter.GreaterThanOrEqualTo -> ">= ${filter.value}" + is DataFilter.LowerThan -> "< ${filter.value}" + is DataFilter.LowerThanOrEqualTo -> "<= ${filter.value}" is DataFilter.Like -> "= '%${filter.value}%'" is DataFilter.LikeIgnoreCase -> "= '%${filter.value}%' COLLATE NOCASE" is DataFilter.NotLike -> "!= '%${filter.value}%'" diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListContext.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListContext.kt new file mode 100644 index 0000000000..b2c74b131f --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListContext.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter + +data class TrackerLineListContext( + val metadata: Map, + val databaseAdapter: DatabaseAdapter, +) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index f549e02809..4e05031465 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -30,7 +30,6 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import android.database.Cursor import org.hisp.dhis.android.core.analytics.AnalyticsException -import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper @@ -61,10 +60,11 @@ internal class TrackerLineListService( // TODO Validate params val metadata = metadataHelper.getMetadata(evaluatedParams) + val context = TrackerLineListContext(metadata, databaseAdapter) val sqlClause = when (evaluatedParams.outputType!!) { - TrackerLineListOutputType.EVENT -> getEventSqlClause(evaluatedParams, metadata) - TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(evaluatedParams, metadata) + TrackerLineListOutputType.EVENT -> getEventSqlClause(evaluatedParams, context) + TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(evaluatedParams, context) } val cursor = databaseAdapter.rawQuery(sqlClause) @@ -103,9 +103,9 @@ internal class TrackerLineListService( .blockingGet() } - private fun getEventSqlClause(params: TrackerLineListParams, metadata: Map): String { + private fun getEventSqlClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return "SELECT " + - "${getEventSelectColumns(params, metadata)} " + + "${getEventSelectColumns(params, context)} " + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + @@ -120,12 +120,12 @@ internal class TrackerLineListService( "WHERE " + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, metadata)} " + "${getEventWhereClause(params, context)} " } - private fun getEnrollmentSqlClause(params: TrackerLineListParams, metadata: Map): String { + private fun getEnrollmentSqlClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return "SELECT " + - "${getEnrollmentSelectColumns(params, metadata)} " + + "${getEnrollmentSelectColumns(params, context)} " + "FROM ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + if (params.hasOrgunit()) { "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + @@ -136,30 +136,30 @@ internal class TrackerLineListService( } + "WHERE " + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "${getEnrollmentWhereClause(params, metadata)} " + "${getEnrollmentWhereClause(params, context)} " } - private fun getEventSelectColumns(params: TrackerLineListParams, metadata: Map): String { + private fun getEventSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(", ") { - "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEvent()}) ${it.id}" + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEvent()}) ${it.id}" } } - private fun getEventWhereClause(params: TrackerLineListParams, metadata: Map): String { + private fun getEventWhereClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(" AND ") { - TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEvent() + TrackerLineListEvaluatorMapper.getEvaluator(it, context).getWhereSQLForEvent() } } - private fun getEnrollmentSelectColumns(params: TrackerLineListParams, metadata: Map): String { + private fun getEnrollmentSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(", ") { - "(${TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getSelectSQLForEnrollment()}) ${it.id}" + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEnrollment()}) ${it.id}" } } - private fun getEnrollmentWhereClause(params: TrackerLineListParams, metadata: Map): String { + private fun getEnrollmentWhereClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(" AND ") { - TrackerLineListEvaluatorMapper.getEvaluator(it, metadata).getWhereSQLForEnrollment() + TrackerLineListEvaluatorMapper.getEvaluator(it, context).getWhereSQLForEnrollment() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index ac92a69e6c..ec3ece85c8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.program.internal.ProgramIndicatorStore import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStore import org.koin.core.annotation.Singleton @@ -38,6 +39,7 @@ import org.koin.core.annotation.Singleton @Suppress("LongParameterList") internal class TrackerLineListServiceMetadataHelper( private val trackedEntityAttributeStore: TrackedEntityAttributeStore, + private val programIndicatorStore: ProgramIndicatorStore, ) { fun getMetadata(params: TrackerLineListParams): Map { @@ -56,6 +58,7 @@ internal class TrackerLineListServiceMetadataHelper( if (!metadata.containsKey(item.id)) { val metadataItems = when (item) { is TrackerLineListItem.ProgramAttribute -> getProgramAttributeItems(item) + is TrackerLineListItem.ProgramIndicator -> getProgramIndicator(item) else -> emptyList() } val metadataItemsMap = metadataItems.associateBy { it.id } @@ -74,4 +77,13 @@ internal class TrackerLineListServiceMetadataHelper( MetadataItem.TrackedEntityAttributeItem(attribute), ) } + + private fun getProgramIndicator(item: TrackerLineListItem.ProgramIndicator): List { + val programIndicator = programIndicatorStore.selectByUid(item.uid) + ?: throw AnalyticsException.InvalidProgramIndicator(item.uid) + + return listOf( + MetadataItem.ProgramIndicatorItem(programIndicator) + ) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt new file mode 100644 index 0000000000..b4f08dac68 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListContext +import org.hisp.dhis.android.core.arch.helpers.UidsHelper +import org.hisp.dhis.android.core.constant.Constant +import org.hisp.dhis.android.core.constant.internal.ConstantStoreImpl +import org.hisp.dhis.android.core.dataelement.internal.DataElementStoreImpl +import org.hisp.dhis.android.core.parser.internal.expression.CommonExpressionVisitor +import org.hisp.dhis.android.core.parser.internal.expression.CommonExpressionVisitorScope +import org.hisp.dhis.android.core.parser.internal.expression.CommonParser +import org.hisp.dhis.android.core.parser.internal.expression.ExpressionItemMethod +import org.hisp.dhis.android.core.parser.internal.expression.ParserUtils +import org.hisp.dhis.android.core.program.ProgramIndicator +import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorItemIdsCollector +import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorParserUtils +import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLContext +import org.hisp.dhis.android.core.program.programindicatorengine.internal.literal.ProgramIndicatorSQLLiteral +import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStoreImpl +import org.hisp.dhis.antlr.Parser + +internal class ProgramIndicatorEvaluator( + private val item: TrackerLineListItem.ProgramIndicator, + private val context: TrackerLineListContext, +) : TrackerLineListEvaluator() { + + private val constantStore = ConstantStoreImpl(context.databaseAdapter) + private val dataElementStore = DataElementStoreImpl(context.databaseAdapter) + private val trackedEntityAttributeStore = TrackedEntityAttributeStoreImpl(context.databaseAdapter) + + override fun getCommonSelectSQL(): String { + val programIndicator = getProgramIndicator() + + val context = ProgramIndicatorSQLContext( + programIndicator = programIndicator, + periods = emptyList(), + ) + + val collector = ProgramIndicatorItemIdsCollector() + Parser.listen(programIndicator.expression(), collector) + + val sqlVisitor = newVisitor(ParserUtils.ITEM_GET_SQL, context) + sqlVisitor.itemIds = collector.itemIds.toMutableSet() + sqlVisitor.setExpressionLiteral(ProgramIndicatorSQLLiteral()) + + val selectExpression = CommonParser.visit(programIndicator.expression(), sqlVisitor) + + val filterExpression = when (programIndicator.filter()?.trim()) { + "true", "", null -> "1" + else -> CommonParser.visit(programIndicator.filter(), sqlVisitor) + } + + return "SELECT CASE ($filterExpression) " + + "WHEN 1 THEN ($selectExpression) " + + "ELSE '' " + + "END" + } + + override fun getCommonWhereSQL(): String { + return DataFilterHelper.getWhereClause(item.id, item.filters) + } + + private fun getProgramIndicator(): ProgramIndicator { + val programIndicatorMetadata = context.metadata[item.id] + ?: throw AnalyticsException.InvalidProgramIndicator(item.id) + + return ((programIndicatorMetadata) as MetadataItem.ProgramIndicatorItem).item + } + + private fun constantMap(): Map { + val constants = constantStore.selectAll() + return UidsHelper.mapByUid(constants) + } + + private fun newVisitor( + itemMethod: ExpressionItemMethod, + context: ProgramIndicatorSQLContext, + ): CommonExpressionVisitor { + return CommonExpressionVisitor( + CommonExpressionVisitorScope.ProgramSQLIndicator( + itemMap = ProgramIndicatorParserUtils.PROGRAM_INDICATOR_SQL_EXPRESSION_ITEMS, + itemMethod = itemMethod, + constantMap = constantMap(), + programIndicatorSQLContext = context, + dataElementStore = dataElementStore, + trackedEntityAttributeStore = trackedEntityAttributeStore, + ), + ) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt index de7bdea8cb..ec44be12e6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -28,15 +28,16 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator -import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListContext internal object TrackerLineListEvaluatorMapper { - fun getEvaluator(item: TrackerLineListItem, metadata: Map): TrackerLineListEvaluator { + fun getEvaluator(item: TrackerLineListItem, context: TrackerLineListContext): TrackerLineListEvaluator { return when (item) { - is TrackerLineListItem.ProgramAttribute -> ProgramAttributeEvaluator(item, metadata) - is TrackerLineListItem.ProgramDataElement -> ProgramDataElementEvaluator(item, metadata) - is TrackerLineListItem.ProgramIndicator -> TODO() + is TrackerLineListItem.ProgramAttribute -> ProgramAttributeEvaluator(item, context.metadata) + is TrackerLineListItem.ProgramDataElement -> ProgramDataElementEvaluator(item, context.metadata) + is TrackerLineListItem.ProgramIndicator -> ProgramIndicatorEvaluator(item, context) + is TrackerLineListItem.OrganisationUnitItem -> OrganisationUnitEvaluator(item) is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt index 43cce6e4ba..69683e38bd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListSQLLabel.kt @@ -28,8 +28,10 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator -object TrackerLineListSQLLabel { - const val EventAlias = "ev" - const val EnrollmentAlias = "en" +import org.hisp.dhis.android.core.program.programindicatorengine.internal.ProgramIndicatorSQLUtils + +internal object TrackerLineListSQLLabel { + const val EventAlias = ProgramIndicatorSQLUtils.event + const val EnrollmentAlias = ProgramIndicatorSQLUtils.enrollment const val OrgunitAlias = "ou" } From 76805bb10444c0b3297aef8c9268524350b93884 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 23 Feb 2024 12:08:25 +0100 Subject: [PATCH 135/222] [ANDROSDK-1809] Add organisationunit filters --- .../evaluator/OrganisationUnitEvaluator.kt | 84 +++++++++++++++++-- .../TrackerLineListEvaluatorMapper.kt | 2 +- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt index 79dc70dae9..c937d5de01 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt @@ -30,12 +30,28 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.trackerlinelist.OrganisationUnitFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListContext import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.OrgunitAlias +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder +import org.hisp.dhis.android.core.common.RelativeOrganisationUnit +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitOrganisationUnitGroupLinkTableInfo import org.hisp.dhis.android.core.organisationunit.OrganisationUnitTableInfo +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitLevelStoreImpl +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitOrganisationUnitGroupLinkStoreImpl +import org.hisp.dhis.android.core.organisationunit.internal.OrganisationUnitStoreImpl +import org.hisp.dhis.android.core.user.internal.UserOrganisationUnitLinkStoreImpl internal class OrganisationUnitEvaluator( private val item: TrackerLineListItem.OrganisationUnitItem, + context: TrackerLineListContext, ) : TrackerLineListEvaluator() { + + private val userOrganisationUnitLinkStore = UserOrganisationUnitLinkStoreImpl(context.databaseAdapter) + private val organisationUnitStore = OrganisationUnitStoreImpl(context.databaseAdapter) + private val orgunitLevelStore = OrganisationUnitLevelStoreImpl(context.databaseAdapter) + private val orgunitGroupLinkStore = OrganisationUnitOrganisationUnitGroupLinkStoreImpl(context.databaseAdapter) + override fun getCommonSelectSQL(): String { return "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.DISPLAY_NAME}" } @@ -50,14 +66,72 @@ internal class OrganisationUnitEvaluator( private fun getFilterWhereClause(filter: OrganisationUnitFilter): String { return when (filter) { - is OrganisationUnitFilter.Absolute -> - "${OrganisationUnitTableInfo.Columns.PATH} LIKE '%${filter.uid}%'" + is OrganisationUnitFilter.Absolute -> inPathOf(filter.uid) + + is OrganisationUnitFilter.Relative -> { + val userAssignedOrgunits = userOrganisationUnitLinkStore + .queryAssignedOrganisationUnitUidsByScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) + + when (filter.relative) { + RelativeOrganisationUnit.USER_ORGUNIT -> { + inPathOfAny(userAssignedOrgunits) + } + + RelativeOrganisationUnit.USER_ORGUNIT_CHILDREN -> { + val children = getChildren(userAssignedOrgunits) + + inPathOfAny(children) + } + + RelativeOrganisationUnit.USER_ORGUNIT_GRANDCHILDREN -> { + val children = getChildren(userAssignedOrgunits) + val grandChildren = getChildren(children) + + inPathOfAny(grandChildren) + } + } + } - is OrganisationUnitFilter.Relative -> TODO() + is OrganisationUnitFilter.Level -> { + val level = orgunitLevelStore.selectByUid(filter.uid) + val orgunits = orgunitLevelStore.selectUidsWhere( + WhereClauseBuilder() + .appendKeyStringValue(OrganisationUnitTableInfo.Columns.LEVEL, level?.level()?.toString()) + .build() + ) - is OrganisationUnitFilter.Level -> TODO() + inPathOfAny(orgunits) + } - is OrganisationUnitFilter.Group -> TODO() + is OrganisationUnitFilter.Group -> { + val orgunits = orgunitGroupLinkStore.selectWhere( + WhereClauseBuilder() + .appendKeyStringValue( + OrganisationUnitOrganisationUnitGroupLinkTableInfo.Columns.ORGANISATION_UNIT, + filter.uid + ) + .build() + ) + + inPathOfAny(orgunits.mapNotNull { it.organisationUnit() }) + } } } + + private fun inPathOfAny(orgunits: List): String { + val orClauses = orgunits.joinToString(" OR ") { ou -> inPathOf(ou) } + return "($orClauses)" + } + + private fun inPathOf(orgunit: String): String { + return "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.PATH} LIKE '%${orgunit}%'" + } + + private fun getChildren(orgunits: List): List { + return organisationUnitStore.selectUidsWhere( + WhereClauseBuilder() + .appendInKeyStringValues(OrganisationUnitTableInfo.Columns.PARENT, orgunits) + .build() + ) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt index ec44be12e6..6c4b5baf68 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -38,7 +38,7 @@ internal object TrackerLineListEvaluatorMapper { is TrackerLineListItem.ProgramDataElement -> ProgramDataElementEvaluator(item, context.metadata) is TrackerLineListItem.ProgramIndicator -> ProgramIndicatorEvaluator(item, context) - is TrackerLineListItem.OrganisationUnitItem -> OrganisationUnitEvaluator(item) + is TrackerLineListItem.OrganisationUnitItem -> OrganisationUnitEvaluator(item, context) is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) is TrackerLineListItem.EventStatusItem -> EventStatusEvaluator(item) From 3abd0725c21bbd549d89d8c44049f66dd8241d3f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 26 Feb 2024 16:38:29 +0100 Subject: [PATCH 136/222] [ANDROSDK-1809] Add support for repeated dataelements --- ...ckerLineListRepositoryIntegrationShould.kt | 74 +++++++++++++++++-- .../trackerlinelist/TrackerLineListModel.kt | 18 ++++- .../TrackerLineListResponse.kt | 9 ++- .../internal/TrackerLineListParams.kt | 39 +++++++++- .../internal/TrackerLineListService.kt | 40 +++++++--- .../TrackerLineListServiceMetadataHelper.kt | 29 ++++++++ .../internal/TrackerVisualizationMapper.kt | 6 +- .../evaluator/ProgramDataElementEvaluator.kt | 45 ++++++++--- .../TrackerLineListRepositoryShould.kt | 8 +- .../internal/TrackerLineListParamsShould.kt | 32 ++++++++ 10 files changed, 261 insertions(+), 39 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index a0280ca5e5..1b612da7ec 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -29,21 +29,44 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist import com.google.common.truth.Truth.assertThat -import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorIntegrationShould +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.dataElement1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.generator +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201911 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201912 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period202001 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.program +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.programStage1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntityType +import org.hisp.dhis.android.core.program.programindicatorengine.BaseTrackerDataIntegrationHelper +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegrationShould() { + + private val helper = BaseTrackerDataIntegrationHelper(databaseAdapter) -class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDispatcher() { @Test fun evaluate_program_attributes() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + helper.createEnrollment(trackedEntity1.uid(), enrollment1, program.uid(), orgunitChild1.uid()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "45") + val result = d2.analyticsModule().trackerLineList() - .withEventOutput("IpHINAT79UW", "dBwrot7S420") + .withEnrollmentOutput(program.uid()) .withColumn(TrackerLineListItem.OrganisationUnitItem(filters = emptyList())) .withColumn( TrackerLineListItem.ProgramAttribute( - uid = "cejWyOfXge6", + uid = attribute1.uid(), filters = listOf( - DataFilter.GreaterThan("400000"), - DataFilter.LowerThan("700000"), + DataFilter.GreaterThan("40"), + DataFilter.LowerThan("50"), ), ), ) @@ -51,4 +74,43 @@ class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDi assertThat(result.getOrThrow().rows.size).isEqualTo(1) } + + @Test + fun evaluate_repeated_data_elements() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + helper.createEnrollment(trackedEntity1.uid(), enrollment1, program.uid(), orgunitChild1.uid()) + val event1 = generator.generate() + helper.createTrackerEvent(event1, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201911.startDate()) + val event2 = generator.generate() + helper.createTrackerEvent(event2, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201912.startDate()) + val event3 = generator.generate() + helper.createTrackerEvent(event3, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period202001.startDate()) + + helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "8") + helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "19") + helper.insertTrackedEntityDataValue(event3, dataElement1.uid(), "2") + + val result1 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn( + TrackerLineListItem.ProgramDataElement( + dataElement = dataElement1.uid(), + program = program.uid(), + programStage = programStage1.uid(), + filters = listOf( + DataFilter.GreaterThan("15") + ), + repetitionIndexes = listOf(1, 2, 0, -1) + ) + ) + .blockingEvaluate() + + val rows = result1.getOrThrow().rows + assertThat(rows.size).isEqualTo(1) + + val row = rows.first() + assertThat(row.size).isEqualTo(4) + + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 1bc64aded5..661e9393c3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -51,11 +51,25 @@ sealed class TrackerLineListItem(val id: String) { data class ProgramAttribute(val uid: String, val filters: List) : TrackerLineListItem(uid) data class ProgramDataElement( - val uid: String, + val dataElement: String, val program: String?, val programStage: String?, val filters: List, - ) : TrackerLineListItem("${program?.let { "$it." } ?: ""}${programStage?.let { "$it." } ?: ""}$uid") + val repetitionIndexes: List? + ) : TrackerLineListItem( + stageDataElementId(dataElement, program, programStage) + + (repetitionIndexes?.joinToString { it.toString() } ?: "")) { + + val stageDataElementIdx = stageDataElementId(dataElement, program, programStage) + + companion object { + fun stageDataElementId(dataElement: String, program: String?, programStage: String?): String { + return (program?.let { "$it." } ?: "") + + (programStage?.let { "$it." } ?: "") + + dataElement + } + } + } object CreatedBy : TrackerLineListItem(Label.CreatedBy) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt index eaf01f2705..e8954b9a10 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt @@ -32,11 +32,16 @@ import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem data class TrackerLineListResponse( val metadata: Map, - val headers: List, - val filters: List, + val headers: List, + val filters: List, val rows: List>, ) +data class TrackerLineListHeader( + val id: String, + val repetitionIndex: Int? = null, +) + data class TrackerLineListValue( val metadataItem: String, val value: String?, diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index 8347261e54..3b7db17076 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -67,6 +67,43 @@ internal data class TrackerLineListParams( } fun hasOrgunit(): Boolean { - return (columns + filters).any { it is TrackerLineListItem.OrganisationUnitItem} + return (columns + filters).any { it is TrackerLineListItem.OrganisationUnitItem } + } + + fun flattenRepeatedDataElements(): TrackerLineListParams { + return this.copy( + columns = flattenRepeatedDataElements(this.columns), + filters = flattenRepeatedDataElements(this.filters), + ) + } + + private fun flattenRepeatedDataElements(items: List): List { + return items.map { item -> + when (item) { + is TrackerLineListItem.ProgramDataElement -> flattenDataElement(item) + else -> listOf(item) + } + }.flatten() + } + + private fun flattenDataElement(item: TrackerLineListItem.ProgramDataElement): List { + val flattenDataElements = + if (item.repetitionIndexes.isNullOrEmpty()) { + listOf(item) + } else { + sortIndexes(item.repetitionIndexes).map { idx -> item.copy(repetitionIndexes = listOf(idx)) } + } + + return flattenDataElements.map { + it.copy( + program = it.program ?: programId, + programStage = it.programStage ?: programStageId, + ) + } + } + + private fun sortIndexes(indexes: List): List { + val (positive, negativeOrZero) = indexes.sorted().partition { it > 0 } + return positive + negativeOrZero } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index 4e05031465..89aebf5310 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import android.database.Cursor import org.hisp.dhis.android.core.analytics.AnalyticsException +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper @@ -86,14 +87,19 @@ internal class TrackerLineListService( } private fun evaluateParams(params: TrackerLineListParams): TrackerLineListParams { - return if (params.trackerVisualization != null) { - val visualization = getTrackerVisualization(params.trackerVisualization) - ?: throw AnalyticsException.InvalidVisualization(params.trackerVisualization) + return params + .run { + if (this.trackerVisualization != null) { + val visualization = getTrackerVisualization(this.trackerVisualization) + ?: throw AnalyticsException.InvalidVisualization(this.trackerVisualization) - trackerVisualizationMapper.toTrackerLineListParams(visualization) + params - } else { - params - } + trackerVisualizationMapper.toTrackerLineListParams(visualization) + this + } else { + this + } + }.run { + this.flattenRepeatedDataElements() + } } private fun getTrackerVisualization(trackerVisualization: String): TrackerVisualization? { @@ -141,7 +147,7 @@ internal class TrackerLineListService( private fun getEventSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(", ") { - "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEvent()}) ${it.id}" + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEvent()}) '${it.id}'" } } @@ -153,13 +159,23 @@ internal class TrackerLineListService( private fun getEnrollmentSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { return params.allItems.joinToString(", ") { - "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEnrollment()}) ${it.id}" + "(${TrackerLineListEvaluatorMapper.getEvaluator(it, context).getSelectSQLForEnrollment()}) '${it.id}'" } } private fun getEnrollmentWhereClause(params: TrackerLineListParams, context: TrackerLineListContext): String { - return params.allItems.joinToString(" AND ") { - TrackerLineListEvaluatorMapper.getEvaluator(it, context).getWhereSQLForEnrollment() + val unflattenedRepeatedDataElements = params.allItems.groupBy { item -> + when (item) { + is TrackerLineListItem.ProgramDataElement -> item.stageDataElementIdx + else -> item.id + } + } + + return unflattenedRepeatedDataElements.values.joinToString(" AND ") { items -> + val orClause = items.joinToString(" OR ") { + TrackerLineListEvaluatorMapper.getEvaluator(it, context).getWhereSQLForEnrollment() + } + "($orClause)" } } @@ -171,7 +187,7 @@ internal class TrackerLineListService( do { val row: MutableList = mutableListOf() params.columns.forEach { item -> - val columnIndex = cursor.getColumnIndex(item.id) + val columnIndex = cursor.columnNames.indexOf(item.id) row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) } values.add(row) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index ec3ece85c8..7767d3c3b9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -31,7 +31,10 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.dataelement.internal.DataElementStore import org.hisp.dhis.android.core.program.internal.ProgramIndicatorStore +import org.hisp.dhis.android.core.program.internal.ProgramStageStore +import org.hisp.dhis.android.core.program.internal.ProgramStore import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeStore import org.koin.core.annotation.Singleton @@ -39,7 +42,10 @@ import org.koin.core.annotation.Singleton @Suppress("LongParameterList") internal class TrackerLineListServiceMetadataHelper( private val trackedEntityAttributeStore: TrackedEntityAttributeStore, + private val dataElementStore: DataElementStore, private val programIndicatorStore: ProgramIndicatorStore, + private val programStore: ProgramStore, + private val programStageStore: ProgramStageStore, ) { fun getMetadata(params: TrackerLineListParams): Map { @@ -58,6 +64,7 @@ internal class TrackerLineListServiceMetadataHelper( if (!metadata.containsKey(item.id)) { val metadataItems = when (item) { is TrackerLineListItem.ProgramAttribute -> getProgramAttributeItems(item) + is TrackerLineListItem.ProgramDataElement -> getProgramDataElement(item) is TrackerLineListItem.ProgramIndicator -> getProgramIndicator(item) else -> emptyList() } @@ -86,4 +93,26 @@ internal class TrackerLineListServiceMetadataHelper( MetadataItem.ProgramIndicatorItem(programIndicator) ) } + + private fun getProgramDataElement(item: TrackerLineListItem.ProgramDataElement): List { + val dataElement = dataElementStore.selectByUid(item.dataElement) + ?: throw AnalyticsException.InvalidDataElement(item.dataElement) + + /*val program = item.program?.let { getProgram(it) } + val programStage = item.programStage?.let { getProgramStage(it) }*/ + + return listOfNotNull( + MetadataItem.DataElementItem(dataElement), + ) + } + + private fun getProgram(programId: String): MetadataItem { + /*return programStore.selectByUid(programId) + ?: throw AnalyticsException.InvalidProgram(programId)*/ + TODO() + } + + private fun getProgramStage(programStageId: String): MetadataItem { + TODO() + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index 04fd586b16..4882868d81 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -151,8 +151,10 @@ internal class TrackerVisualizationMapper( private fun mapProgramDataElement(item: TrackerVisualizationDimension): TrackerLineListItem? { return item.dimension()?.let { uid -> - TrackerLineListItem - .ProgramDataElement(uid, item.program()?.uid(), item.programStage()?.uid(), mapDataFilters(item)) + TrackerLineListItem.ProgramDataElement( + uid, item.program()?.uid(), item.programStage()?.uid(), + mapDataFilters(item), item.repetition()?.indexes() + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt index 5f395a5a27..f0d06560a8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt @@ -32,7 +32,9 @@ import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.event.EventTableInfo import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo import org.hisp.dhis.android.core.util.SqlUtils.getColumnValueCast @@ -42,27 +44,50 @@ internal class ProgramDataElementEvaluator( private val metadata: Map, ) : TrackerLineListEvaluator() { override fun getSelectSQLForEvent(): String { - return "SELECT ${getColumnSql()} " + - "FROM ${TrackedEntityDataValueTableInfo.TABLE_INFO.name()} " + - "WHERE ${TrackedEntityDataValueTableInfo.Columns.EVENT} = " + - "$EventAlias.${EventTableInfo.Columns.UID} " + - "AND ${TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT} = '${item.uid}'" + val selectEventClause = "= $EventAlias.${EventTableInfo.Columns.UID} " + + return getSelectClause(selectEventClause) } override fun getSelectSQLForEnrollment(): String { - return TODO() + /** eventIdx meaning: + * -> 0: newest event + * -> -1: newest event - 1 (second newest event) + * -> 1: oldest event + * -> 2: oldest event - 1 (second oldest event + */ + val eventIdx = item.repetitionIndexes?.firstOrNull() ?: 0 + + val eventSelectClause = "IN (SELECT ${EventTableInfo.Columns.UID} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} " + + "WHERE ${EventTableInfo.Columns.ENROLLMENT} = $EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + (item.programStage?.let { "AND ${EventTableInfo.Columns.PROGRAM_STAGE} = '$it' " } ?: "") + + "ORDER BY ${EventTableInfo.Columns.EVENT_DATE} ${if (eventIdx <= 0) "DESC" else "ASC"} " + + "LIMIT 1 " + + "OFFSET ${ + if (eventIdx <= 0) { + -eventIdx + } else { + eventIdx - 1 + } + })" + + return getSelectClause(eventSelectClause) } - override fun getWhereSQLForEvent(): String { + override fun getCommonWhereSQL(): String { return DataFilterHelper.getWhereClause(item.id, item.filters) } - override fun getWhereSQLForEnrollment(): String { - return TODO() + private fun getSelectClause(selectEventClause: String): String { + return "SELECT ${getColumnSql()} " + + "FROM ${TrackedEntityDataValueTableInfo.TABLE_INFO.name()} " + + "WHERE ${TrackedEntityDataValueTableInfo.Columns.EVENT} $selectEventClause " + + "AND ${TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT} = '${item.dataElement}'" } private fun getColumnSql(): String { - val dataElementMetadata = metadata[item.id] ?: throw AnalyticsException.InvalidDataElement(item.id) + val dataElementMetadata = metadata[item.dataElement] ?: throw AnalyticsException.InvalidDataElement(item.id) val dataElement = ((dataElementMetadata) as MetadataItem.DataElementItem).item return getColumnValueCast( diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt index 7ceb7dae32..3f0a2b82d9 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt @@ -51,9 +51,9 @@ class TrackerLineListRepositoryShould { @Test fun `Call service with overridden columns`() { - val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "program", "programStage", listOf()) + val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "program", "programStage", listOf(), null) val de1_2 = de1_1.copy(filters = listOf(DataFilter.EqualTo("value"))) - val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "program", "programStage", listOf()) + val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "program", "programStage", listOf(), null) repository .withColumn(de1_1) @@ -67,8 +67,8 @@ class TrackerLineListRepositoryShould { assertThat(columns.size).isEqualTo(2) val dataElementColumns = columns.filterIsInstance() - val de1 = dataElementColumns.find { it.uid == "dataElement1" }!! - val de2 = dataElementColumns.find { it.uid == "dataElement2" }!! + val de1 = dataElementColumns.find { it.dataElement == "dataElement1" }!! + val de2 = dataElementColumns.find { it.dataElement == "dataElement2" }!! assertThat(de1.filters.size).isEqualTo(1) assertThat(de2.filters.size).isEqualTo(0) diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt index 3ac47d43c7..e5e0ccd0fd 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt @@ -80,4 +80,36 @@ class TrackerLineListParamsShould { TrackerLineListItem.DateItem.EventDate(listOf(DateFilter.Absolute("202405"))), ) } + + @Test + fun should_flatten_repeated_data_elements() { + val params = TrackerLineListParams( + trackerVisualization = null, + outputType = TrackerLineListOutputType.ENROLLMENT, + programId = "programId", + programStageId = null, + columns = listOf( + TrackerLineListItem.ProgramDataElement("dataElement", null, null, listOf(), listOf(0, -1, -2, 1, 2)) + ), + filters = emptyList(), + ) + + val flattenedParams = params.flattenRepeatedDataElements() + + assertThat(flattenedParams.columns.size).isEqualTo(5) + + flattenedParams.columns.map { it as TrackerLineListItem.ProgramDataElement }.forEachIndexed { index, item -> + when (index) { + 0 -> assertIndex(item, 1) + 1 -> assertIndex(item, 2) + 2 -> assertIndex(item, -2) + 3 -> assertIndex(item, -1) + 4 -> assertIndex(item, 0) + } + } + } + + private fun assertIndex(item: TrackerLineListItem.ProgramDataElement, idx: Int) { + assertThat(item.repetitionIndexes!!.first()).isEqualTo(idx) + } } From 9e917e316b76116965da91daadf42084217f5981 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 27 Feb 2024 16:09:53 +0100 Subject: [PATCH 137/222] [ANDROSDK-1809] Add metadata, header and filter information --- .../evaluator/BaseEvaluatorSamples.kt | 5 ++ ...ckerLineListRepositoryIntegrationShould.kt | 12 ++++- .../core/analytics/AnalyticsException.kt | 1 + .../analytics/aggregated/AnalyticsModel.kt | 3 ++ .../internal/AnalyticsModelHelper.kt | 35 +++++++++++++ .../trackerlinelist/TrackerLineListModel.kt | 13 ++--- .../TrackerLineListResponse.kt | 11 ++-- .../internal/TrackerLineListService.kt | 27 ++-------- .../internal/TrackerLineListServiceHelper.kt | 52 +++++++++++++++++++ .../TrackerLineListServiceMetadataHelper.kt | 28 +++++----- .../evaluator/ProgramDataElementEvaluator.kt | 3 +- 11 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt index ec74e947b6..c2e264a8d6 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt @@ -275,6 +275,8 @@ object BaseEvaluatorSamples { val program: Program = Program.builder() .uid(generator.generate()) + .name("Tracker program") + .displayName("Tracker program") .trackedEntityType(trackedEntityType) .categoryCombo(ObjectWithUid.create(categoryCombo.uid())) .build() @@ -282,12 +284,15 @@ object BaseEvaluatorSamples { val programStage1: ProgramStage = ProgramStage.builder() .uid(generator.generate()) .name("Program stage 1") + .displayName("Program stage 1") .program(ObjectWithUid.create(program.uid())) .formType(FormType.DEFAULT) .build() val programStage2: ProgramStage = ProgramStage.builder() .uid(generator.generate()) + .name("Program stage 2") + .displayName("Program stage 2") .program(ObjectWithUid.create(program.uid())) .formType(FormType.DEFAULT) .build() diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index 1b612da7ec..89a9755d96 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -81,11 +81,11 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra val enrollment1 = generator.generate() helper.createEnrollment(trackedEntity1.uid(), enrollment1, program.uid(), orgunitChild1.uid()) val event1 = generator.generate() - helper.createTrackerEvent(event1, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201911.startDate()) + helper.createTrackerEvent(event1, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period202001.startDate()) val event2 = generator.generate() helper.createTrackerEvent(event2, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201912.startDate()) val event3 = generator.generate() - helper.createTrackerEvent(event3, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period202001.startDate()) + helper.createTrackerEvent(event3, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201911.startDate()) helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "8") helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "19") @@ -112,5 +112,13 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra val row = rows.first() assertThat(row.size).isEqualTo(4) + rows.first().forEachIndexed { index, value -> + when (index) { + 0 -> assertThat(value.value).isEqualTo("2") + 1 -> assertThat(value.value).isEqualTo("19") + 2 -> assertThat(value.value).isEqualTo("19") + 3 -> assertThat(value.value).isEqualTo("8") + } + } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsException.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsException.kt index 77be65b168..ce0fd65fe7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsException.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/AnalyticsException.kt @@ -38,6 +38,7 @@ sealed class AnalyticsException(message: String) : Throwable(message) { class InvalidDataElementOperand(val uid: String) : AnalyticsException("Missing DataElementOperand $uid") class InvalidProgramIndicator(val uid: String) : AnalyticsException("Missing ProgramIndicator $uid") class InvalidProgram(val uid: String) : AnalyticsException("Missing Program $uid") + class InvalidProgramStage(val uid: String) : AnalyticsException("Missing ProgramStage $uid") class InvalidIndicator(val uid: String) : AnalyticsException("Missing Indicator $uid") class InvalidExpressionDimensionItem(val uid: String) : AnalyticsException("Missing ExpressionDimensionItem $uid") class InvalidOrganisationUnit(val uid: String) : AnalyticsException("Missing organisation unit $uid") diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt index 18174a6864..59e197394d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/aggregated/AnalyticsModel.kt @@ -43,6 +43,7 @@ import org.hisp.dhis.android.core.organisationunit.OrganisationUnitLevel import org.hisp.dhis.android.core.period.Period import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramIndicator +import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute sealed class MetadataItem(val id: String, val displayName: String) { @@ -54,6 +55,8 @@ sealed class MetadataItem(val id: String, val displayName: String) { class IndicatorItem(val item: Indicator) : MetadataItem(item.uid(), item.displayName()!!) class ProgramIndicatorItem(val item: ProgramIndicator) : MetadataItem(item.uid(), item.displayName()!!) + class ProgramItem(val item: Program) : MetadataItem(item.uid(), item.displayName()!!) + class ProgramStageItem(val item: ProgramStage) : MetadataItem(item.uid(), item.displayName()!!) class EventDataElementItem(val item: DataElement, val program: Program) : MetadataItem("${program.uid()}.${item.uid()}", "${program.displayName()} ${item.displayName()}") class EventAttributeItem(val item: TrackedEntityAttribute, val program: Program) : diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt new file mode 100644 index 0000000000..d79fa824e4 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.internal + +internal object AnalyticsModelHelper { + fun eventDataElementId(program: String?, programStage: String?, dataElement: String): String { + return listOfNotNull(program, programStage, dataElement).joinToString(".") + } +} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 661e9393c3..f8d5891bf8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist +import org.hisp.dhis.android.core.analytics.internal.AnalyticsModelHelper.eventDataElementId import org.hisp.dhis.android.core.common.RelativeOrganisationUnit import org.hisp.dhis.android.core.common.RelativePeriod import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -57,18 +58,10 @@ sealed class TrackerLineListItem(val id: String) { val filters: List, val repetitionIndexes: List? ) : TrackerLineListItem( - stageDataElementId(dataElement, program, programStage) + + eventDataElementId(program, programStage, dataElement) + (repetitionIndexes?.joinToString { it.toString() } ?: "")) { - val stageDataElementIdx = stageDataElementId(dataElement, program, programStage) - - companion object { - fun stageDataElementId(dataElement: String, program: String?, programStage: String?): String { - return (program?.let { "$it." } ?: "") + - (programStage?.let { "$it." } ?: "") + - dataElement - } - } + val stageDataElementIdx = eventDataElementId(program, programStage, dataElement) } object CreatedBy : TrackerLineListItem(Label.CreatedBy) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt index e8954b9a10..00d5d5a09d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListResponse.kt @@ -32,17 +32,12 @@ import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem data class TrackerLineListResponse( val metadata: Map, - val headers: List, - val filters: List, + val headers: List, + val filters: List, val rows: List>, ) -data class TrackerLineListHeader( - val id: String, - val repetitionIndex: Int? = null, -) - data class TrackerLineListValue( - val metadataItem: String, + val id: String, val value: String?, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index 89aebf5310..7a97f606d1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -28,11 +28,10 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal -import android.database.Cursor import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse -import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue +import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListServiceHelper.mapCursorToColumns import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListEvaluatorMapper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias @@ -74,8 +73,8 @@ internal class TrackerLineListService( Result.Success( TrackerLineListResponse( metadata = metadata, - headers = emptyList(), - filters = emptyList(), + headers = evaluatedParams.columns, + filters = evaluatedParams.filters, rows = values, ), ) @@ -164,7 +163,7 @@ internal class TrackerLineListService( } private fun getEnrollmentWhereClause(params: TrackerLineListParams, context: TrackerLineListContext): String { - val unflattenedRepeatedDataElements = params.allItems.groupBy { item -> + val unflattenedRepeatedDataElements = params.allItems.groupBy { item -> when (item) { is TrackerLineListItem.ProgramDataElement -> item.stageDataElementIdx else -> item.id @@ -178,22 +177,4 @@ internal class TrackerLineListService( "($orClause)" } } - - private fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List> { - val values: MutableList> = mutableListOf() - cursor.use { c -> - if (c.count > 0) { - c.moveToFirst() - do { - val row: MutableList = mutableListOf() - params.columns.forEach { item -> - val columnIndex = cursor.columnNames.indexOf(item.id) - row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) - } - values.add(row) - } while (c.moveToNext()) - } - } - return values - } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt new file mode 100644 index 0000000000..41c385929e --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal + +import android.database.Cursor +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue + +internal object TrackerLineListServiceHelper { + fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List> { + val values: MutableList> = mutableListOf() + cursor.use { c -> + if (c.count > 0) { + c.moveToFirst() + do { + val row: MutableList = mutableListOf() + params.columns.forEach { item -> + val columnIndex = cursor.columnNames.indexOf(item.id) + row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) + } + values.add(row) + } while (c.moveToNext()) + } + } + return values + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index 7767d3c3b9..42cbb04489 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -32,6 +32,8 @@ import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.dataelement.internal.DataElementStore +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.program.internal.ProgramIndicatorStore import org.hisp.dhis.android.core.program.internal.ProgramStageStore import org.hisp.dhis.android.core.program.internal.ProgramStore @@ -39,7 +41,6 @@ import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityAttributeS import org.koin.core.annotation.Singleton @Singleton -@Suppress("LongParameterList") internal class TrackerLineListServiceMetadataHelper( private val trackedEntityAttributeStore: TrackedEntityAttributeStore, private val dataElementStore: DataElementStore, @@ -96,23 +97,26 @@ internal class TrackerLineListServiceMetadataHelper( private fun getProgramDataElement(item: TrackerLineListItem.ProgramDataElement): List { val dataElement = dataElementStore.selectByUid(item.dataElement) + ?.let { MetadataItem.DataElementItem(it) } ?: throw AnalyticsException.InvalidDataElement(item.dataElement) - /*val program = item.program?.let { getProgram(it) } - val programStage = item.programStage?.let { getProgramStage(it) }*/ + val program = item.program?.let { getProgram(it) } + ?.let { MetadataItem.ProgramItem(it) } + ?: throw AnalyticsException.InvalidArguments("DataElement ${item.dataElement} has no program defined") - return listOfNotNull( - MetadataItem.DataElementItem(dataElement), - ) + val programStage = item.programStage?.let { getProgramStage(it) } + ?.let { MetadataItem.ProgramStageItem(it) } + + return listOfNotNull(dataElement, program, programStage) } - private fun getProgram(programId: String): MetadataItem { - /*return programStore.selectByUid(programId) - ?: throw AnalyticsException.InvalidProgram(programId)*/ - TODO() + private fun getProgram(programId: String): Program { + return programStore.selectByUid(programId) + ?: throw AnalyticsException.InvalidProgram(programId) } - private fun getProgramStage(programStageId: String): MetadataItem { - TODO() + private fun getProgramStage(programStageId: String): ProgramStage { + return programStageStore.selectByUid(programStageId) + ?: throw AnalyticsException.InvalidProgramStage(programStageId) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt index f0d06560a8..dafea2c6e5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt @@ -87,7 +87,8 @@ internal class ProgramDataElementEvaluator( } private fun getColumnSql(): String { - val dataElementMetadata = metadata[item.dataElement] ?: throw AnalyticsException.InvalidDataElement(item.id) + val dataElementMetadata = metadata[item.dataElement] + ?: throw AnalyticsException.InvalidDataElement(item.id) val dataElement = ((dataElementMetadata) as MetadataItem.DataElementItem).item return getColumnValueCast( From 5954b7a98075a57942692a5407c248e806c31980 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 28 Feb 2024 12:27:03 +0100 Subject: [PATCH 138/222] [ANDROSDK-1750] Update DHIS, patch and SMS versions --- .../org/hisp/dhis/android/core/systeminfo/DHISVersion.kt | 8 +++++--- .../dhis/android/core/systeminfo/DHISVersionManager.kt | 1 + .../org/hisp/dhis/android/core/systeminfo/SMSVersion.kt | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt index 0da0d7025f..c4405d7e16 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt @@ -41,17 +41,19 @@ enum class DHISVersion(internal val prefix: String, internal val supported: Bool V2_39("2.39"), V2_40("2.40"), V2_41("2.41"), + UNKNOWN("UNKNOWN", false), ; companion object { @JvmStatic - fun getValue(versionStr: String): DHISVersion? { + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): DHISVersion? { return entries.find { versionStr.startsWith(it.prefix).and(it.supported) } + ?: bypassDHIS2VersionCheck.takeIf { it == true }?.let { UNKNOWN } } @JvmStatic - fun isAllowedVersion(versionStr: String): Boolean { - return getValue(versionStr) != null + fun isAllowedVersion(versionStr: String, bypassDHIS2VersionCheck: Boolean?): Boolean { + return getValue(versionStr, bypassDHIS2VersionCheck) != null } @JvmStatic diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt index 4c12f2d6cc..6f9e867a27 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt @@ -31,6 +31,7 @@ interface DHISVersionManager { fun getVersion(): DHISVersion fun getPatchVersion(): DHISPatchVersion? fun getSmsVersion(): SMSVersion? + fun getBypassVersion(): Boolean? /** * Check if the current version is equal to the version passed as parameter. diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt index 43e26d0e24..0d32934d19 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt @@ -34,18 +34,18 @@ enum class SMSVersion(val intValue: Int) { companion object { @JvmStatic - fun getValue(versionStr: String): SMSVersion? { - val patchVersion = DHISPatchVersion.getValue(versionStr) + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): SMSVersion? { + val patchVersion = DHISPatchVersion.getValue(versionStr, bypassDHIS2VersionCheck) return if (patchVersion == null) { - DHISVersion.getValue(versionStr)?.let { getLatestInDHISVersion(it) } + DHISVersion.getValue(versionStr, bypassDHIS2VersionCheck)?.let { getLatestInDHISVersion(it) } } else { patchVersion.smsVersion } } private fun getLatestInDHISVersion(dhisVersion: DHISVersion): SMSVersion? { - return DHISPatchVersion.values() + return DHISPatchVersion.entries .filter { it.majorVersion == dhisVersion && it.smsVersion != null } .fold(null) { latest: SMSVersion?, version -> if (latest == null || latest.intValue < version.smsVersion!!.intValue) { From ac2635d4b79f2f30bede3b68438d99dd81355073 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 28 Feb 2024 12:27:50 +0100 Subject: [PATCH 139/222] [ANDROSDK-1750] Update patch version --- .../hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt index 28cbefc438..87048795f5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt @@ -69,12 +69,15 @@ enum class DHISPatchVersion(val majorVersion: DHISVersion, val strValue: String, V2_40_0(DHISVersion.V2_40, "2.40.0", SMSVersion.V2), V2_41_0(DHISVersion.V2_41, "2.41.0", SMSVersion.V2), + + UNKNOWN(DHISVersion.UNKNOWN, "UNKNOWN", SMSVersion.V2), ; companion object { @JvmStatic - fun getValue(versionStr: String): DHISPatchVersion? { - return values().find { versionStr == it.strValue || versionStr.startsWith(it.strValue + "-") } + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): DHISPatchVersion? { + return entries.find { versionStr == it.strValue || versionStr.startsWith(it.strValue + "-") } + ?: bypassDHIS2VersionCheck.takeIf { it == true }?.let { UNKNOWN } } } } From 748e4c4ec27e4e907a2dbe972c67b21ee9eee6a8 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 28 Feb 2024 13:57:48 +0100 Subject: [PATCH 140/222] [ANDROSDK-1750] Use version manager to handle the bypass status check --- .../core/settings/GeneralSettingTableInfo.kt | 2 +- .../settings/internal/GeneralSettingCall.kt | 5 ++++- .../core/systeminfo/DHISPatchVersion.kt | 2 +- .../android/core/systeminfo/DHISVersion.kt | 4 ++-- .../core/systeminfo/DHISVersionManager.kt | 4 ++++ .../android/core/systeminfo/SMSVersion.kt | 2 +- .../internal/DHISVersionManagerImpl.kt | 22 ++++++++++++++----- .../systeminfo/internal/SystemInfoCall.kt | 2 +- 8 files changed, 30 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt index ec664a40a2..b3051e9af1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/GeneralSettingTableInfo.kt @@ -74,4 +74,4 @@ object GeneralSettingTableInfo { const val BYPASS_DHIS2_VERSION_CHECK = "bypassDHIS2VersionCheck" } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCall.kt index 42bbc74cb6..3e6a85a922 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCall.kt @@ -31,6 +31,7 @@ import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallEx import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.GeneralSettings +import org.hisp.dhis.android.core.systeminfo.internal.DHISVersionManagerImpl import org.koin.core.annotation.Singleton @Singleton @@ -39,6 +40,7 @@ internal class GeneralSettingCall( private val settingAppService: SettingAppService, private val appVersionManager: SettingsAppInfoManager, coroutineAPICallExecutor: CoroutineAPICallExecutor, + private val versionManager: DHISVersionManagerImpl, ) : BaseSettingCall(coroutineAPICallExecutor) { private var cachedValue: GeneralSettings? = null @@ -59,6 +61,7 @@ internal class GeneralSettingCall( override fun process(item: GeneralSettings?) { cachedValue = item val generalSettingsList = listOfNotNull(item) + versionManager.setBypassVersion(item?.bypassDHIS2VersionCheck()) generalSettingHandler.handleMany(generalSettingsList) } @@ -67,6 +70,6 @@ internal class GeneralSettingCall( appVersionManager.updateAppVersion() return coroutineAPICallExecutor.wrap(storeError = false) { settingAppService.generalSettings(appVersionManager.getDataStoreVersion()) - }.getOrThrow().encryptDB() + }.getOrThrow().also { versionManager.setBypassVersion(it.bypassDHIS2VersionCheck()) }.encryptDB() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt index 87048795f5..54e39897ec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISPatchVersion.kt @@ -75,7 +75,7 @@ enum class DHISPatchVersion(val majorVersion: DHISVersion, val strValue: String, companion object { @JvmStatic - fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): DHISPatchVersion? { + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean? = false): DHISPatchVersion? { return entries.find { versionStr == it.strValue || versionStr.startsWith(it.strValue + "-") } ?: bypassDHIS2VersionCheck.takeIf { it == true }?.let { UNKNOWN } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt index c4405d7e16..d8fd0117a1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersion.kt @@ -46,13 +46,13 @@ enum class DHISVersion(internal val prefix: String, internal val supported: Bool companion object { @JvmStatic - fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): DHISVersion? { + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean? = false): DHISVersion? { return entries.find { versionStr.startsWith(it.prefix).and(it.supported) } ?: bypassDHIS2VersionCheck.takeIf { it == true }?.let { UNKNOWN } } @JvmStatic - fun isAllowedVersion(versionStr: String, bypassDHIS2VersionCheck: Boolean?): Boolean { + fun isAllowedVersion(versionStr: String, bypassDHIS2VersionCheck: Boolean? = false): Boolean { return getValue(versionStr, bypassDHIS2VersionCheck) != null } diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt index 6f9e867a27..4c681eeb89 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt @@ -27,6 +27,8 @@ */ package org.hisp.dhis.android.core.systeminfo +import androidx.annotation.VisibleForTesting + interface DHISVersionManager { fun getVersion(): DHISVersion fun getPatchVersion(): DHISPatchVersion? @@ -56,4 +58,6 @@ interface DHISVersionManager { * @return True if current version is greater or equal than the parameter. */ fun isGreaterOrEqualThan(version: DHISVersion): Boolean + + fun setBypassVersion(bypassDHIS2VersionCheck: Boolean?) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt index 0d32934d19..e743b650db 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/SMSVersion.kt @@ -34,7 +34,7 @@ enum class SMSVersion(val intValue: Int) { companion object { @JvmStatic - fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean?): SMSVersion? { + fun getValue(versionStr: String, bypassDHIS2VersionCheck: Boolean? = false): SMSVersion? { val patchVersion = DHISPatchVersion.getValue(versionStr, bypassDHIS2VersionCheck) return if (patchVersion == null) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt index 74b0115a17..7a4dce9803 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt @@ -27,6 +27,7 @@ */ package org.hisp.dhis.android.core.systeminfo.internal +import androidx.annotation.VisibleForTesting import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.maintenance.D2ErrorComponent @@ -43,11 +44,12 @@ internal class DHISVersionManagerImpl internal constructor( private var version: DHISVersion? = null private var patchVersion: DHISPatchVersion? = null private var smsVersion: SMSVersion? = null + private var bypassDHIS2Version: Boolean? = null override fun getVersion(): DHISVersion { return version ?: systemInfoStore.selectFirst()?.let { systemInfo -> - systemInfo.version()?.let { DHISVersion.getValue(it) } + systemInfo.version()?.let { DHISVersion.getValue(it, getBypassVersion()) } .also { dhisVersion -> version = dhisVersion } } ?: throw D2Error.builder() @@ -60,7 +62,7 @@ internal class DHISVersionManagerImpl internal constructor( override fun getPatchVersion(): DHISPatchVersion? { return patchVersion ?: systemInfoStore.selectFirst()?.let { systemInfo -> - systemInfo.version()?.let { DHISPatchVersion.getValue(it) } + systemInfo.version()?.let { DHISPatchVersion.getValue(it, getBypassVersion()) } .also { patch -> patchVersion = patch } } } @@ -68,11 +70,15 @@ internal class DHISVersionManagerImpl internal constructor( override fun getSmsVersion(): SMSVersion? { return smsVersion ?: systemInfoStore.selectFirst()?.let { systemInfo -> - systemInfo.version()?.let { SMSVersion.getValue(it) } + systemInfo.version()?.let { SMSVersion.getValue(it, getBypassVersion()) } .also { sms -> smsVersion = sms } } } + override fun getBypassVersion(): Boolean? { + return bypassDHIS2Version + } + override fun isVersion(version: DHISVersion): Boolean { return version === getVersion() } @@ -86,8 +92,12 @@ internal class DHISVersionManagerImpl internal constructor( } internal fun setVersion(versionStr: String) { - version = DHISVersion.getValue(versionStr) - patchVersion = DHISPatchVersion.getValue(versionStr) - smsVersion = SMSVersion.getValue(versionStr) + version = DHISVersion.getValue(versionStr, getBypassVersion()) + patchVersion = DHISPatchVersion.getValue(versionStr, getBypassVersion()) + smsVersion = SMSVersion.getValue(versionStr, getBypassVersion()) + } + + override fun setBypassVersion(bypassDHIS2VersionCheck: Boolean?) { + bypassDHIS2Version = bypassDHIS2VersionCheck } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCall.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCall.kt index e76ee45e50..03c4c3ecfd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCall.kt @@ -55,7 +55,7 @@ class SystemInfoCall internal constructor( }.fold( onSuccess = { systemInfo -> val version = systemInfo.version() - if (version != null && isAllowedVersion(version)) { + if (version != null && isAllowedVersion(version, versionManager.getBypassVersion())) { versionManager.setVersion(version) } else { throw D2Error.builder() From 9cbe80d45f59a45d0d55a70cd38876f708596437 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 28 Feb 2024 13:58:06 +0100 Subject: [PATCH 141/222] [ANDROSDK-1750] Update unit tests --- .../internal/GeneralSettingCallShould.kt | 16 ++++++++++- .../core/systeminfo/SMSVersionShould.kt | 8 +++++- .../internal/DHISVersionManagerShould.kt | 27 +++++++++++++++++++ .../internal/SystemInfoCallShould.kt | 8 ++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCallShould.kt index 8bbcc5ebc5..43ba634a52 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/GeneralSettingCallShould.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.test.runTest import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutorMock import org.hisp.dhis.android.core.maintenance.D2ErrorSamples import org.hisp.dhis.android.core.settings.GeneralSettings +import org.hisp.dhis.android.core.systeminfo.internal.DHISVersionManagerImpl import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -47,6 +48,7 @@ class GeneralSettingCallShould { private val generalSetting: GeneralSettings = mock() private val coroutineAPICallExecutor: CoroutineAPICallExecutorMock = CoroutineAPICallExecutorMock() private val appVersionManager: SettingsAppInfoManager = mock() + private val versionManager: DHISVersionManagerImpl = mock() private lateinit var generalSettingCall: GeneralSettingCall @@ -56,7 +58,10 @@ class GeneralSettingCallShould { onBlocking { getDataStoreVersion() } doReturn SettingsAppDataStoreVersion.V1_1 } whenAPICall { generalSetting } - generalSettingCall = GeneralSettingCall(handler, service, appVersionManager, coroutineAPICallExecutor) + whenever(generalSetting.bypassDHIS2VersionCheck()).thenReturn(true) + generalSettingCall = GeneralSettingCall( + handler, service, appVersionManager, coroutineAPICallExecutor, versionManager, + ) } private fun whenAPICall(answer: Answer) { @@ -73,4 +78,13 @@ class GeneralSettingCallShould { verify(handler).handleMany(emptyList()) verifyNoMoreInteractions(handler) } + + @Test + fun handle_general_setting_and_set_bypass_DHIS2_version() = runTest { + whenever(service.generalSettings(any())) doAnswer { generalSetting } + + generalSettingCall.download(false) + verify(handler).handleMany(listOfNotNull(generalSetting)) + verify(versionManager).setBypassVersion(true) + } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt index 5ba5bf262c..35e0d3b200 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt @@ -57,9 +57,15 @@ class SMSVersionShould { assertThat(smsVersion).isNull() } + @Test + fun return_unknown_version_if_patch_does_not_exist_but_bypass_is_true() { + val smsVersion = getValue("2.32.100", true) + assertThat(smsVersion).isEqualTo(DHISPatchVersion.UNKNOWN.smsVersion) + } + @Test fun return_non_null_for_any_version_greater_than_2_32() { - DHISVersion.values() + DHISVersion.entries .filter { it > DHISVersion.V2_32 && it.supported } .forEach { assertThat(getValue(it.prefix + ".0")).isNotNull() diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt index 3ee9d335b6..d0b98b61b1 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt @@ -60,16 +60,36 @@ class DHISVersionManagerShould { assertThat(dhisVersionManager.isVersion(DHISVersion.V2_31)).isTrue() assertThat(dhisVersionManager.isVersion(DHISVersion.V2_32)).isFalse() assertThat(dhisVersionManager.isVersion(DHISVersion.V2_33)).isFalse() + assertThat(dhisVersionManager.isVersion(DHISVersion.UNKNOWN)).isFalse() assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_30)).isTrue() assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_31)).isFalse() assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_32)).isFalse() assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_33)).isFalse() + assertThat(dhisVersionManager.isGreaterThan(DHISVersion.UNKNOWN)).isFalse() assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_30)).isTrue() assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_31)).isTrue() assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_32)).isFalse() assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_33)).isFalse() + assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.UNKNOWN)).isFalse() + } + + @Test + fun compare_version_when_unknown() { + dhisVersionManager.setBypassVersion(true) + whenever(systemInfo.version()).thenReturn(DHISVersion.UNKNOWN.name) + + assertThat(dhisVersionManager.isVersion(DHISVersion.V2_33)).isFalse() + assertThat(dhisVersionManager.isVersion(DHISVersion.UNKNOWN)).isTrue() + + assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_32)).isTrue() + assertThat(dhisVersionManager.isGreaterThan(DHISVersion.V2_33)).isTrue() + assertThat(dhisVersionManager.isGreaterThan(DHISVersion.UNKNOWN)).isFalse() + + assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_32)).isTrue() + assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_33)).isTrue() + assertThat(dhisVersionManager.isGreaterOrEqualThan(DHISVersion.UNKNOWN)).isTrue() } @Test(expected = D2Error::class) @@ -90,6 +110,13 @@ class DHISVersionManagerShould { assertThat(dhisVersionManager.getPatchVersion()).isNull() } + @Test + fun return_unknown_if_unknown_patch_version_and_bypass_dhis2_version_is_true() { + dhisVersionManager.setBypassVersion(true) + whenever(systemInfo.version()).thenReturn("2.47.59") + assertThat(dhisVersionManager.getPatchVersion()).isEqualTo(DHISPatchVersion.UNKNOWN) + } + @Test fun should_return_sms_version() { whenever(systemInfo.version()).thenReturn("2.39.5.1") diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCallShould.kt index b14ab87523..ebd7af911e 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/SystemInfoCallShould.kt @@ -128,6 +128,14 @@ class SystemInfoCallShould { verify(resourceHandler).handleResource(eq(Resource.Type.SYSTEM_INFO)) } + @Test + fun invoke_set_version_and_check_bypass_Version_after_successful_call() = runTest { + systemInfoSyncCall.download(true) + + verify(versionManager).getBypassVersion() + verify(versionManager).setVersion(any()) + } + @Test fun throw_d2_call_exception_when_system_version_not_supported() = runTest { whenever(systemInfo.version()).thenReturn("2.28") From 388954fe61e28634a07af6f937b7be4e849e55cb Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 17:20:54 +0100 Subject: [PATCH 142/222] [ANDROSDK-1809] Add integration tests --- ...ackerLineListRepositoryDispatcherShould.kt | 49 ++++++ ...ckerLineListRepositoryIntegrationShould.kt | 159 +++++++++++++++++- .../BaseTrackerDataIntegrationHelper.kt | 15 +- ...llectionRepositoryMockIntegrationShould.kt | 5 +- .../internal/AnalyticsModelHelper.kt | 2 +- .../trackerlinelist/TrackerLineListModel.kt | 49 ++++-- .../internal/TrackerLineListService.kt | 63 +++---- .../internal/TrackerLineListServiceHelper.kt | 22 ++- .../TrackerLineListServiceMetadataHelper.kt | 2 +- .../internal/TrackerVisualizationMapper.kt | 17 +- .../internal/evaluator/BaseDateEvaluator.kt | 10 +- .../evaluator/EnrollmentDateEvaluator.kt | 2 +- .../internal/evaluator/EventDateEvaluator.kt | 2 +- .../evaluator/IncidentDateEvaluator.kt | 2 +- .../evaluator/LastUpdatedEvaluator.kt | 2 +- .../evaluator/OrganisationUnitEvaluator.kt | 10 +- .../evaluator/ProgramDataElementEvaluator.kt | 30 ++-- .../evaluator/ProgramIndicatorEvaluator.kt | 6 +- .../evaluator/ScheduledDateEvaluator.kt | 2 +- .../evaluator/TrackerLineListEvaluator.kt | 6 +- .../TrackerLineListEvaluatorMapper.kt | 10 +- .../tracker_visualizations_1.json | 5 +- .../internal/TrackerLineListParamsShould.kt | 12 +- 23 files changed, 359 insertions(+), 123 deletions(-) create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt new file mode 100644 index 0000000000..94eef5f134 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +internal class TrackerLineListRepositoryDispatcherShould : BaseMockIntegrationTestFullDispatcher() { + + @Test + fun evaluate_program_attributes() { + val result = d2.analyticsModule().trackerLineList() + .withTrackerVisualization("s85urBIkN0z") + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(2) + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index 89a9755d96..3a467e70eb 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -40,11 +40,17 @@ import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEv import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.program import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.programStage1 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity2 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntityType +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.AnalyticsType +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.program.programindicatorengine.BaseTrackerDataIntegrationHelper import org.hisp.dhis.android.core.utils.runner.D2JunitRunner import org.junit.Test import org.junit.runner.RunWith +import java.util.Date @RunWith(D2JunitRunner::class) internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegrationShould() { @@ -55,7 +61,7 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra fun evaluate_program_attributes() { helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) val enrollment1 = generator.generate() - helper.createEnrollment(trackedEntity1.uid(), enrollment1, program.uid(), orgunitChild1.uid()) + createDefaultEnrollment(trackedEntity1.uid(), enrollment1) helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "45") val result = d2.analyticsModule().trackerLineList() @@ -79,13 +85,13 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra fun evaluate_repeated_data_elements() { helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) val enrollment1 = generator.generate() - helper.createEnrollment(trackedEntity1.uid(), enrollment1, program.uid(), orgunitChild1.uid()) + createDefaultEnrollment(trackedEntity1.uid(), enrollment1) val event1 = generator.generate() - helper.createTrackerEvent(event1, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period202001.startDate()) + createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) val event2 = generator.generate() - helper.createTrackerEvent(event2, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201912.startDate()) + createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) val event3 = generator.generate() - helper.createTrackerEvent(event3, enrollment1, program.uid(), programStage1.uid(), orgunitChild1.uid(), eventDate = period201911.startDate()) + createDefaultTrackerEvent(event3, enrollment1, eventDate = period201911.startDate()) helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "8") helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "19") @@ -99,10 +105,10 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra program = program.uid(), programStage = programStage1.uid(), filters = listOf( - DataFilter.GreaterThan("15") + DataFilter.GreaterThan("15"), ), - repetitionIndexes = listOf(1, 2, 0, -1) - ) + repetitionIndexes = listOf(1, 2, 0, -1), + ), ) .blockingEvaluate() @@ -121,4 +127,141 @@ internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegra } } } + + @Test + fun should_filter_by_date() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") + + helper.createTrackedEntity(trackedEntity2.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment2 = generator.generate() + createDefaultEnrollment(trackedEntity2.uid(), enrollment2, enrollmentDate = period201912.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity2.uid(), attribute1.uid(), "789") + + // Filter by absolute value + val result1 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) + .withColumn( + TrackerLineListItem.EnrollmentDate( + filters = listOf(DateFilter.Absolute("2020")), + ), + ) + .blockingEvaluate() + + val rows1 = result1.getOrThrow().rows + assertThat(rows1.size).isEqualTo(1) + assertThat(rows1[0][0].value).isEqualTo("123") + assertThat(rows1[0][1].value).isEqualTo("2020-01-01T00:00:00.000") + + // Filter by range + val result2 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) + .withColumn( + TrackerLineListItem.EnrollmentDate( + filters = listOf( + DateFilter.Range( + startDate = DateUtils.DATE_FORMAT.format(period201911.startDate()!!), + endDate = DateUtils.DATE_FORMAT.format(period201912.endDate()!!), + ), + ), + ), + ) + .blockingEvaluate() + + val rows2 = result2.getOrThrow().rows + assertThat(rows2.size).isEqualTo(1) + assertThat(rows2[0][0].value).isEqualTo("789") + assertThat(rows2[0][1].value).isEqualTo("2019-12-01T00:00:00.000") + } + + @Test + fun evaluate_program_indicator() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") + + val event1 = generator.generate() + createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) + val event2 = generator.generate() + createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) + + helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "5") + helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "10") + + val programIndicator = generator.generate() + helper.setProgramIndicatorExpression( + programIndicator, + program.uid(), + expression = "A{${attribute1.uid()}} + #{${programStage1.uid()}.${dataElement1.uid()}}", + analyticsType = AnalyticsType.EVENT, + ) + + val result = d2.analyticsModule().trackerLineList() + .withEventOutput(program.uid(), programStage1.uid()) + .withColumn(TrackerLineListItem.EventDate()) + .withColumn(TrackerLineListItem.ProgramIndicator(programIndicator)) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(2) + assertThat(rows[0][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period202001.startDate()!!)) + assertThat(rows[0][1].value).isEqualTo("128") + assertThat(rows[1][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period201912.startDate()!!)) + assertThat(rows[1][1].value).isEqualTo("133") + } + + private fun createDefaultEnrollment( + teiUid: String, + enrollmentUid: String, + programUid: String = program.uid(), + orgunitUid: String = orgunitChild1.uid(), + enrollmentDate: Date? = null, + incidentDate: Date? = null, + created: Date? = null, + lastUpdated: Date? = null, + status: EnrollmentStatus? = EnrollmentStatus.ACTIVE, + ) { + helper.createEnrollment( + teiUid, + enrollmentUid, + programUid, + orgunitUid, + enrollmentDate, + incidentDate, + created, + lastUpdated, + status, + ) + } + + private fun createDefaultTrackerEvent( + eventUid: String, + enrollmentUid: String, + programUid: String = program.uid(), + programStageUid: String = programStage1.uid(), + orgunitUid: String = orgunitChild1.uid(), + deleted: Boolean = false, + eventDate: Date? = null, + created: Date? = null, + lastUpdated: Date? = null, + status: EventStatus? = EventStatus.ACTIVE, + ) { + helper.createTrackerEvent( + eventUid, + enrollmentUid, + programUid, + programStageUid, + orgunitUid, + deleted, + eventDate, + created, + lastUpdated, + status, + ) + } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/program/programindicatorengine/BaseTrackerDataIntegrationHelper.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/program/programindicatorengine/BaseTrackerDataIntegrationHelper.kt index 76253ad9be..f986e2dbe8 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/program/programindicatorengine/BaseTrackerDataIntegrationHelper.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/program/programindicatorengine/BaseTrackerDataIntegrationHelper.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.program.programindicatorengine import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.common.AggregationType +import org.hisp.dhis.android.core.common.AnalyticsType import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -151,8 +152,9 @@ open class BaseTrackerDataIntegrationHelper(private val databaseAdapter: Databas programIndicatorUid: String, programUid: String, expression: String, + analyticsType: AnalyticsType, ) { - insertProgramIndicator(programIndicatorUid, programUid, expression, AggregationType.AVERAGE) + insertProgramIndicator(programIndicatorUid, programUid, expression, AggregationType.AVERAGE, analyticsType) } fun insertProgramIndicator( @@ -160,9 +162,16 @@ open class BaseTrackerDataIntegrationHelper(private val databaseAdapter: Databas programUid: String, expression: String, aggregationType: AggregationType, + analyticsType: AnalyticsType = AnalyticsType.ENROLLMENT, ) { - val programIndicator = ProgramIndicator.builder().uid(programIndicatorUid) - .program(ObjectWithUid.create(programUid)).expression(expression).aggregationType(aggregationType).build() + val programIndicator = ProgramIndicator.builder() + .uid(programIndicatorUid) + .name(programIndicatorUid) + .displayName(programIndicatorUid) + .analyticsType(analyticsType) + .program(ObjectWithUid.create(programUid)).expression(expression) + .aggregationType(aggregationType) + .build() setProgramIndicator(programIndicator) } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt index f20ba449cd..7ed6a6af6c 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/visualization/TrackerVisualizationCollectionRepositoryMockIntegrationShould.kt @@ -122,7 +122,8 @@ class TrackerVisualizationCollectionRepositoryMockIntegrationShould : assertThat(visualization.filters()!!.size).isEqualTo(1) assertThat(visualization.filters()!![0].dimension()).isEqualTo("enrollmentDate") assertThat(visualization.filters()!![0].dimensionType()).isEqualTo("PERIOD") - assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(1) - assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("LAST_10_YEARS") + assertThat(visualization.filters()!![0].items()!!.size).isEqualTo(2) + assertThat(visualization.filters()!![0].items()!![0].uid()).isEqualTo("2018") + assertThat(visualization.filters()!![0].items()!![1].uid()).isEqualTo("2019") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt index d79fa824e4..da82998d9c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt @@ -32,4 +32,4 @@ internal object AnalyticsModelHelper { fun eventDataElementId(program: String?, programStage: String?, dataElement: String): String { return listOfNotNull(program, programStage, dataElement).joinToString(".") } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index f8d5891bf8..6ded9a7afb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -36,41 +36,58 @@ import org.hisp.dhis.android.core.event.EventStatus sealed class TrackerLineListItem(val id: String) { - class OrganisationUnitItem(val filters: List) : + data class OrganisationUnitItem(val filters: List) : TrackerLineListItem(Label.OrganisationUnit) - sealed class DateItem(id: String, val filters: List) : TrackerLineListItem(id) { - class LastUpdated(filters: List) : DateItem(Label.LastUpdated, filters) - class IncidentDate(filters: List) : DateItem(Label.IncidentDate, filters) - class EnrollmentDate(filters: List) : DateItem(Label.EnrollmentDate, filters) - class ScheduledDate(filters: List) : DateItem(Label.ScheduledDate, filters) - class EventDate(filters: List) : DateItem(Label.EventDate, filters) - } + data class LastUpdated(override val filters: List = emptyList()) : + TrackerLineListItem(Label.LastUpdated), DateItem + + data class IncidentDate(override val filters: List = emptyList()) : + TrackerLineListItem(Label.IncidentDate), DateItem + + data class EnrollmentDate(override val filters: List = emptyList()) : + TrackerLineListItem(Label.EnrollmentDate), DateItem + + data class ScheduledDate(override val filters: List = emptyList()) : + TrackerLineListItem(Label.ScheduledDate), DateItem + + data class EventDate(override val filters: List = emptyList()) : + TrackerLineListItem(Label.EventDate), DateItem - data class ProgramIndicator(val uid: String, val filters: List) : TrackerLineListItem(uid) + data class ProgramIndicator(val uid: String, val filters: List = emptyList()) : + TrackerLineListItem(uid) - data class ProgramAttribute(val uid: String, val filters: List) : TrackerLineListItem(uid) + data class ProgramAttribute(val uid: String, val filters: List = emptyList()) : + TrackerLineListItem(uid) data class ProgramDataElement( val dataElement: String, val program: String?, val programStage: String?, - val filters: List, - val repetitionIndexes: List? + val filters: List = emptyList(), + val repetitionIndexes: List? = null, ) : TrackerLineListItem( eventDataElementId(program, programStage, dataElement) + - (repetitionIndexes?.joinToString { it.toString() } ?: "")) { + (repetitionIndexes?.joinToString { it.toString() } ?: ""), + ) { val stageDataElementIdx = eventDataElementId(program, programStage, dataElement) } + data class ProgramStatusItem(val filters: List = emptyList()) : + TrackerLineListItem(Label.ProgramStatus) + + data class EventStatusItem(val filters: List = emptyList()) : + TrackerLineListItem(Label.EventStatus) + object CreatedBy : TrackerLineListItem(Label.CreatedBy) object LastUpdatedBy : TrackerLineListItem(Label.LastUpdatedBy) +} - data class ProgramStatusItem(val filters: List) : TrackerLineListItem(Label.ProgramStatus) - - data class EventStatusItem(val filters: List) : TrackerLineListItem(Label.EventStatus) +internal interface DateItem { + val id: String + val filters: List } sealed class OrganisationUnitFilter { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index 7a97f606d1..a9d968cc99 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -53,16 +53,19 @@ internal class TrackerLineListService( private val metadataHelper: TrackerLineListServiceMetadataHelper, private val trackerVisualizationMapper: TrackerVisualizationMapper, ) { + @Suppress("TooGenericExceptionCaught") fun evaluate(params: TrackerLineListParams): Result { return try { val evaluatedParams = evaluateParams(params) - // TODO Validate params + if (evaluatedParams.outputType == null) { + throw AnalyticsException.InvalidArguments("Output type cannot be empty.") + } val metadata = metadataHelper.getMetadata(evaluatedParams) val context = TrackerLineListContext(metadata, databaseAdapter) - val sqlClause = when (evaluatedParams.outputType!!) { + val sqlClause = when (evaluatedParams.outputType) { TrackerLineListOutputType.EVENT -> getEventSqlClause(evaluatedParams, context) TrackerLineListOutputType.ENROLLMENT -> getEnrollmentSqlClause(evaluatedParams, context) } @@ -110,38 +113,38 @@ internal class TrackerLineListService( private fun getEventSqlClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return "SELECT " + - "${getEventSelectColumns(params, context)} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + - "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - if (params.hasOrgunit()) { - "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + - "ON $EventAlias.${EventTableInfo.Columns.ORGANISATION_UNIT} = " + - "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " - } else { - "" - } + - "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, context)} " + "${getEventSelectColumns(params, context)} " + + "FROM ${EventTableInfo.TABLE_INFO.name()} $EventAlias " + + "LEFT JOIN ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ENROLLMENT} = " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + if (params.hasOrgunit()) { + "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + + "ON $EventAlias.${EventTableInfo.Columns.ORGANISATION_UNIT} = " + + "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " + } else { + "" + } + + "WHERE " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + + "${getEventWhereClause(params, context)} " } private fun getEnrollmentSqlClause(params: TrackerLineListParams, context: TrackerLineListContext): String { return "SELECT " + - "${getEnrollmentSelectColumns(params, context)} " + - "FROM ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + - if (params.hasOrgunit()) { - "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + - "ON $EnrollmentAlias.${EnrollmentTableInfo.Columns.ORGANISATION_UNIT} = " + - "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " - } else { - "" - } + - "WHERE " + - "$EnrollmentAlias.${EnrollmentTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "${getEnrollmentWhereClause(params, context)} " + "${getEnrollmentSelectColumns(params, context)} " + + "FROM ${EnrollmentTableInfo.TABLE_INFO.name()} $EnrollmentAlias " + + if (params.hasOrgunit()) { + "LEFT JOIN ${OrganisationUnitTableInfo.TABLE_INFO.name()} $OrgunitAlias " + + "ON $EnrollmentAlias.${EnrollmentTableInfo.Columns.ORGANISATION_UNIT} = " + + "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.UID} " + } else { + "" + } + + "WHERE " + + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + + "${getEnrollmentWhereClause(params, context)} " } private fun getEventSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt index 41c385929e..2f3ac504cc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceHelper.kt @@ -29,24 +29,30 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import android.database.Cursor +import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListValue internal object TrackerLineListServiceHelper { fun mapCursorToColumns(params: TrackerLineListParams, cursor: Cursor): List> { - val values: MutableList> = mutableListOf() + val rows: MutableList> = mutableListOf() cursor.use { c -> if (c.count > 0) { c.moveToFirst() do { - val row: MutableList = mutableListOf() - params.columns.forEach { item -> - val columnIndex = cursor.columnNames.indexOf(item.id) - row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) - } - values.add(row) + val row = mapRowValues(cursor, params.columns) + rows.add(row) } while (c.moveToNext()) } } - return values + return rows + } + + private fun mapRowValues(cursor: Cursor, columns: List): List { + val row: MutableList = mutableListOf() + columns.forEach { item -> + val columnIndex = cursor.columnNames.indexOf(item.id) + row.add(TrackerLineListValue(item.id, cursor.getString(columnIndex))) + } + return row } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index 42cbb04489..1c36394d17 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -91,7 +91,7 @@ internal class TrackerLineListServiceMetadataHelper( ?: throw AnalyticsException.InvalidProgramIndicator(item.uid) return listOf( - MetadataItem.ProgramIndicatorItem(programIndicator) + MetadataItem.ProgramIndicatorItem(programIndicator), ) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index 4882868d81..de1e07c189 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -128,11 +128,11 @@ internal class TrackerVisualizationMapper( private fun mapPeriod(item: TrackerVisualizationDimension): TrackerLineListItem? { return when (item.dimension()) { - "lastUpdated" -> TrackerLineListItem.DateItem.LastUpdated(mapDateFilters(item)) - "incidentDate" -> TrackerLineListItem.DateItem.IncidentDate(mapDateFilters(item)) - "enrollmentDate" -> TrackerLineListItem.DateItem.EnrollmentDate(mapDateFilters(item)) - "scheduledDate" -> TrackerLineListItem.DateItem.ScheduledDate(mapDateFilters(item)) - "eventDate" -> TrackerLineListItem.DateItem.EventDate(mapDateFilters(item)) + "lastUpdated" -> TrackerLineListItem.LastUpdated(mapDateFilters(item)) + "incidentDate" -> TrackerLineListItem.IncidentDate(mapDateFilters(item)) + "enrollmentDate" -> TrackerLineListItem.EnrollmentDate(mapDateFilters(item)) + "scheduledDate" -> TrackerLineListItem.ScheduledDate(mapDateFilters(item)) + "eventDate" -> TrackerLineListItem.EventDate(mapDateFilters(item)) else -> null } } @@ -152,8 +152,11 @@ internal class TrackerVisualizationMapper( private fun mapProgramDataElement(item: TrackerVisualizationDimension): TrackerLineListItem? { return item.dimension()?.let { uid -> TrackerLineListItem.ProgramDataElement( - uid, item.program()?.uid(), item.programStage()?.uid(), - mapDataFilters(item), item.repetition()?.indexes() + uid, + item.program()?.uid(), + item.programStage()?.uid(), + mapDataFilters(item), + item.repetition()?.indexes(), ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt index ec8385a7f8..b7b6c4e2ec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt @@ -29,7 +29,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter -import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.analytics.trackerlinelist.DateItem import org.hisp.dhis.android.core.arch.helpers.DateUtils import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.period.internal.CalendarProviderFactory @@ -38,7 +38,7 @@ import org.hisp.dhis.android.core.period.internal.PeriodParser import java.util.Date internal abstract class BaseDateEvaluator( - private val item: TrackerLineListItem.DateItem, + private val item: DateItem, ) : TrackerLineListEvaluator() { private val parentPeriodGenerator = ParentPeriodGeneratorImpl.create(CalendarProviderFactory.calendarProvider) @@ -48,7 +48,7 @@ internal abstract class BaseDateEvaluator( return if (item.filters.isEmpty()) { "1" } else { - return item.filters.joinToString(" AND ") { getFilterWhereClause(it) } + return item.filters.joinToString(" OR ") { "(${getFilterWhereClause(it)})" } } } private fun getFilterWhereClause(filter: DateFilter): String { @@ -76,11 +76,11 @@ internal abstract class BaseDateEvaluator( private fun betweenDates(startDate: Date, endDate: Date): String { return betweenDates( startDate = DateUtils.DATE_FORMAT.format(startDate), - endDate = DateUtils.DATE_FORMAT.format(endDate) + endDate = DateUtils.DATE_FORMAT.format(endDate), ) } private fun betweenDates(startDate: String, endDate: String): String { - return "julianday(${item.id}) > julianday('$startDate') AND julianday(${item.id}) < julianday('$endDate')" + return "julianday(${item.id}) >= julianday('$startDate') AND julianday(${item.id}) <= julianday('$endDate')" } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt index f99bce9834..7807af42d4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EnrollmentDateEvaluator.kt @@ -33,7 +33,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.T import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo internal class EnrollmentDateEvaluator( - item: TrackerLineListItem.DateItem.EnrollmentDate, + item: TrackerLineListItem.EnrollmentDate, ) : BaseDateEvaluator(item) { override fun getCommonSelectSQL(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt index b761b4c43e..66e3d674e5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventDateEvaluator.kt @@ -34,7 +34,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.T import org.hisp.dhis.android.core.event.EventTableInfo internal class EventDateEvaluator( - item: TrackerLineListItem.DateItem.EventDate, + item: TrackerLineListItem.EventDate, ) : BaseDateEvaluator(item) { override fun getSelectSQLForEvent(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt index 43e687c07c..3c88014ded 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/IncidentDateEvaluator.kt @@ -33,7 +33,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.T import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo internal class IncidentDateEvaluator( - item: TrackerLineListItem.DateItem.IncidentDate, + item: TrackerLineListItem.IncidentDate, ) : BaseDateEvaluator(item) { override fun getCommonSelectSQL(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt index 8c8726879c..90fabe017f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/LastUpdatedEvaluator.kt @@ -35,7 +35,7 @@ import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.event.EventTableInfo internal class LastUpdatedEvaluator( - item: TrackerLineListItem.DateItem.LastUpdated, + item: TrackerLineListItem.LastUpdated, ) : BaseDateEvaluator(item) { override fun getSelectSQLForEvent(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt index c937d5de01..83df77348a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt @@ -97,7 +97,7 @@ internal class OrganisationUnitEvaluator( val orgunits = orgunitLevelStore.selectUidsWhere( WhereClauseBuilder() .appendKeyStringValue(OrganisationUnitTableInfo.Columns.LEVEL, level?.level()?.toString()) - .build() + .build(), ) inPathOfAny(orgunits) @@ -108,9 +108,9 @@ internal class OrganisationUnitEvaluator( WhereClauseBuilder() .appendKeyStringValue( OrganisationUnitOrganisationUnitGroupLinkTableInfo.Columns.ORGANISATION_UNIT, - filter.uid + filter.uid, ) - .build() + .build(), ) inPathOfAny(orgunits.mapNotNull { it.organisationUnit() }) @@ -124,14 +124,14 @@ internal class OrganisationUnitEvaluator( } private fun inPathOf(orgunit: String): String { - return "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.PATH} LIKE '%${orgunit}%'" + return "$OrgunitAlias.${OrganisationUnitTableInfo.Columns.PATH} LIKE '%$orgunit%'" } private fun getChildren(orgunits: List): List { return organisationUnitStore.selectUidsWhere( WhereClauseBuilder() .appendInKeyStringValues(OrganisationUnitTableInfo.Columns.PARENT, orgunits) - .build() + .build(), ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt index dafea2c6e5..0d22a8eef6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt @@ -59,18 +59,18 @@ internal class ProgramDataElementEvaluator( val eventIdx = item.repetitionIndexes?.firstOrNull() ?: 0 val eventSelectClause = "IN (SELECT ${EventTableInfo.Columns.UID} " + - "FROM ${EventTableInfo.TABLE_INFO.name()} " + - "WHERE ${EventTableInfo.Columns.ENROLLMENT} = $EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + - (item.programStage?.let { "AND ${EventTableInfo.Columns.PROGRAM_STAGE} = '$it' " } ?: "") + - "ORDER BY ${EventTableInfo.Columns.EVENT_DATE} ${if (eventIdx <= 0) "DESC" else "ASC"} " + - "LIMIT 1 " + - "OFFSET ${ - if (eventIdx <= 0) { - -eventIdx - } else { - eventIdx - 1 - } - })" + "FROM ${EventTableInfo.TABLE_INFO.name()} " + + "WHERE ${EventTableInfo.Columns.ENROLLMENT} = $EnrollmentAlias.${EnrollmentTableInfo.Columns.UID} " + + (item.programStage?.let { "AND ${EventTableInfo.Columns.PROGRAM_STAGE} = '$it' " } ?: "") + + "ORDER BY ${EventTableInfo.Columns.EVENT_DATE} ${if (eventIdx <= 0) "DESC" else "ASC"} " + + "LIMIT 1 " + + "OFFSET ${ + if (eventIdx <= 0) { + -eventIdx + } else { + eventIdx - 1 + } + })" return getSelectClause(eventSelectClause) } @@ -81,9 +81,9 @@ internal class ProgramDataElementEvaluator( private fun getSelectClause(selectEventClause: String): String { return "SELECT ${getColumnSql()} " + - "FROM ${TrackedEntityDataValueTableInfo.TABLE_INFO.name()} " + - "WHERE ${TrackedEntityDataValueTableInfo.Columns.EVENT} $selectEventClause " + - "AND ${TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT} = '${item.dataElement}'" + "FROM ${TrackedEntityDataValueTableInfo.TABLE_INFO.name()} " + + "WHERE ${TrackedEntityDataValueTableInfo.Columns.EVENT} $selectEventClause " + + "AND ${TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT} = '${item.dataElement}'" } private fun getColumnSql(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt index b4f08dac68..4941b0be97 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt @@ -82,9 +82,9 @@ internal class ProgramIndicatorEvaluator( } return "SELECT CASE ($filterExpression) " + - "WHEN 1 THEN ($selectExpression) " + - "ELSE '' " + - "END" + "WHEN 1 THEN ($selectExpression) " + + "ELSE '' " + + "END" } override fun getCommonWhereSQL(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt index e66c789bee..bbbd8c4be9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ScheduledDateEvaluator.kt @@ -34,7 +34,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.T import org.hisp.dhis.android.core.event.EventTableInfo internal class ScheduledDateEvaluator( - item: TrackerLineListItem.DateItem.ScheduledDate, + item: TrackerLineListItem.ScheduledDate, ) : BaseDateEvaluator(item) { override fun getSelectSQLForEvent(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt index 5cb65da779..3567d0ecda 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluator.kt @@ -28,6 +28,8 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator +import org.hisp.dhis.android.core.analytics.AnalyticsException + internal open class TrackerLineListEvaluator { open fun getSelectSQLForEvent(): String { return getCommonSelectSQL() @@ -43,10 +45,10 @@ internal open class TrackerLineListEvaluator { } protected open fun getCommonSelectSQL(): String { - throw RuntimeException("Not implemented") + throw AnalyticsException.SQLException("SELECT clause is not implemented for this evaluator") } protected open fun getCommonWhereSQL(): String { - throw RuntimeException("Not implemented") + throw AnalyticsException.SQLException("WHERE clause is not implemented for this evaluator") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt index 6c4b5baf68..54557bea8d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/TrackerLineListEvaluatorMapper.kt @@ -43,11 +43,11 @@ internal object TrackerLineListEvaluatorMapper { is TrackerLineListItem.ProgramStatusItem -> ProgramStatusEvaluator(item) is TrackerLineListItem.EventStatusItem -> EventStatusEvaluator(item) - is TrackerLineListItem.DateItem.LastUpdated -> LastUpdatedEvaluator(item) - is TrackerLineListItem.DateItem.IncidentDate -> IncidentDateEvaluator(item) - is TrackerLineListItem.DateItem.EnrollmentDate -> EnrollmentDateEvaluator(item) - is TrackerLineListItem.DateItem.ScheduledDate -> ScheduledDateEvaluator(item) - is TrackerLineListItem.DateItem.EventDate -> EventDateEvaluator(item) + is TrackerLineListItem.LastUpdated -> LastUpdatedEvaluator(item) + is TrackerLineListItem.IncidentDate -> IncidentDateEvaluator(item) + is TrackerLineListItem.EnrollmentDate -> EnrollmentDateEvaluator(item) + is TrackerLineListItem.ScheduledDate -> ScheduledDateEvaluator(item) + is TrackerLineListItem.EventDate -> EventDateEvaluator(item) is TrackerLineListItem.CreatedBy -> NotSupportedEvaluator() is TrackerLineListItem.LastUpdatedBy -> NotSupportedEvaluator() diff --git a/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json index 9b0898d440..f2c4a6015d 100644 --- a/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json +++ b/core/src/sharedTest/resources/visualization/tracker_visualizations_1.json @@ -13,7 +13,10 @@ "dimensionType": "PERIOD", "items": [ { - "id": "LAST_10_YEARS" + "id": "2018" + }, + { + "id": "2019" } ], "dimension": "enrollmentDate" diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt index e5e0ccd0fd..0f5cf08c65 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt @@ -47,8 +47,8 @@ class TrackerLineListParamsShould { programStageId = "program_stage_uid", columns = listOf( TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.GreaterThan("5"))), - TrackerLineListItem.ProgramIndicator("indicator", listOf()), - TrackerLineListItem.DateItem.EventDate(listOf()), + TrackerLineListItem.ProgramIndicator("indicator"), + TrackerLineListItem.EventDate(), ), filters = listOf(), ) @@ -62,7 +62,7 @@ class TrackerLineListParamsShould { TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.NotEqualTo("10"))), ), filters = listOf( - TrackerLineListItem.DateItem.EventDate(listOf(DateFilter.Absolute("202405"))), + TrackerLineListItem.EventDate(listOf(DateFilter.Absolute("202405"))), ), ) @@ -73,11 +73,11 @@ class TrackerLineListParamsShould { assertThat(params.programId).isEqualTo("program_uid") assertThat(params.programStageId).isEqualTo("program_stage_uid") assertThat(params.columns).containsExactly( - TrackerLineListItem.ProgramIndicator("indicator", listOf()), + TrackerLineListItem.ProgramIndicator("indicator"), TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.NotEqualTo("10"))), ) assertThat(params.filters).containsExactly( - TrackerLineListItem.DateItem.EventDate(listOf(DateFilter.Absolute("202405"))), + TrackerLineListItem.EventDate(listOf(DateFilter.Absolute("202405"))), ) } @@ -89,7 +89,7 @@ class TrackerLineListParamsShould { programId = "programId", programStageId = null, columns = listOf( - TrackerLineListItem.ProgramDataElement("dataElement", null, null, listOf(), listOf(0, -1, -2, 1, 2)) + TrackerLineListItem.ProgramDataElement("dataElement", null, null, listOf(), listOf(0, -1, -2, 1, 2)), ), filters = emptyList(), ) From 028324c30333ca8b7178d1cc223a1a02bb094b2f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 17:25:30 +0100 Subject: [PATCH 143/222] [ANDROSDK-1809] Refactor test names --- ...ackerLineListRepositoryDispatcherShould.kt | 49 ---- ...rackerLineListRepositoryEvaluatorShould.kt | 267 ++++++++++++++++++ ...ckerLineListRepositoryIntegrationShould.kt | 226 +-------------- 3 files changed, 271 insertions(+), 271 deletions(-) delete mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt create mode 100644 core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt deleted file mode 100644 index 94eef5f134..0000000000 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryDispatcherShould.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2004-2024, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.analytics.trackerlinelist - -import com.google.common.truth.Truth.assertThat -import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher -import org.hisp.dhis.android.core.utils.runner.D2JunitRunner -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(D2JunitRunner::class) -internal class TrackerLineListRepositoryDispatcherShould : BaseMockIntegrationTestFullDispatcher() { - - @Test - fun evaluate_program_attributes() { - val result = d2.analyticsModule().trackerLineList() - .withTrackerVisualization("s85urBIkN0z") - .blockingEvaluate() - - val rows = result.getOrThrow().rows - assertThat(rows.size).isEqualTo(2) - } -} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt new file mode 100644 index 0000000000..188c9901cb --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorIntegrationShould +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.dataElement1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.generator +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201911 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201912 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period202001 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.program +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.programStage1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity2 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntityType +import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.common.AnalyticsType +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.program.programindicatorengine.BaseTrackerDataIntegrationHelper +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +@RunWith(D2JunitRunner::class) +internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrationShould() { + + private val helper = BaseTrackerDataIntegrationHelper(databaseAdapter) + + @Test + fun evaluate_program_attributes() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "45") + + val result = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.OrganisationUnitItem(filters = emptyList())) + .withColumn( + TrackerLineListItem.ProgramAttribute( + uid = attribute1.uid(), + filters = listOf( + DataFilter.GreaterThan("40"), + DataFilter.LowerThan("50"), + ), + ), + ) + .blockingEvaluate() + + assertThat(result.getOrThrow().rows.size).isEqualTo(1) + } + + @Test + fun evaluate_repeated_data_elements() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1) + val event1 = generator.generate() + createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) + val event2 = generator.generate() + createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) + val event3 = generator.generate() + createDefaultTrackerEvent(event3, enrollment1, eventDate = period201911.startDate()) + + helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "8") + helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "19") + helper.insertTrackedEntityDataValue(event3, dataElement1.uid(), "2") + + val result1 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn( + TrackerLineListItem.ProgramDataElement( + dataElement = dataElement1.uid(), + program = program.uid(), + programStage = programStage1.uid(), + filters = listOf( + DataFilter.GreaterThan("15"), + ), + repetitionIndexes = listOf(1, 2, 0, -1), + ), + ) + .blockingEvaluate() + + val rows = result1.getOrThrow().rows + assertThat(rows.size).isEqualTo(1) + + val row = rows.first() + assertThat(row.size).isEqualTo(4) + + rows.first().forEachIndexed { index, value -> + when (index) { + 0 -> assertThat(value.value).isEqualTo("2") + 1 -> assertThat(value.value).isEqualTo("19") + 2 -> assertThat(value.value).isEqualTo("19") + 3 -> assertThat(value.value).isEqualTo("8") + } + } + } + + @Test + fun should_filter_by_date() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") + + helper.createTrackedEntity(trackedEntity2.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment2 = generator.generate() + createDefaultEnrollment(trackedEntity2.uid(), enrollment2, enrollmentDate = period201912.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity2.uid(), attribute1.uid(), "789") + + // Filter by absolute value + val result1 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) + .withColumn( + TrackerLineListItem.EnrollmentDate( + filters = listOf(DateFilter.Absolute("2020")), + ), + ) + .blockingEvaluate() + + val rows1 = result1.getOrThrow().rows + assertThat(rows1.size).isEqualTo(1) + assertThat(rows1[0][0].value).isEqualTo("123") + assertThat(rows1[0][1].value).isEqualTo("2020-01-01T00:00:00.000") + + // Filter by range + val result2 = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) + .withColumn( + TrackerLineListItem.EnrollmentDate( + filters = listOf( + DateFilter.Range( + startDate = DateUtils.DATE_FORMAT.format(period201911.startDate()!!), + endDate = DateUtils.DATE_FORMAT.format(period201912.endDate()!!), + ), + ), + ), + ) + .blockingEvaluate() + + val rows2 = result2.getOrThrow().rows + assertThat(rows2.size).isEqualTo(1) + assertThat(rows2[0][0].value).isEqualTo("789") + assertThat(rows2[0][1].value).isEqualTo("2019-12-01T00:00:00.000") + } + + @Test + fun evaluate_program_indicator() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") + + val event1 = generator.generate() + createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) + val event2 = generator.generate() + createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) + + helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "5") + helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "10") + + val programIndicator = generator.generate() + helper.setProgramIndicatorExpression( + programIndicator, + program.uid(), + expression = "A{${attribute1.uid()}} + #{${programStage1.uid()}.${dataElement1.uid()}}", + analyticsType = AnalyticsType.EVENT, + ) + + val result = d2.analyticsModule().trackerLineList() + .withEventOutput(program.uid(), programStage1.uid()) + .withColumn(TrackerLineListItem.EventDate()) + .withColumn(TrackerLineListItem.ProgramIndicator(programIndicator)) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(2) + assertThat(rows[0][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period202001.startDate()!!)) + assertThat(rows[0][1].value).isEqualTo("128") + assertThat(rows[1][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period201912.startDate()!!)) + assertThat(rows[1][1].value).isEqualTo("133") + } + + private fun createDefaultEnrollment( + teiUid: String, + enrollmentUid: String, + programUid: String = program.uid(), + orgunitUid: String = orgunitChild1.uid(), + enrollmentDate: Date? = null, + incidentDate: Date? = null, + created: Date? = null, + lastUpdated: Date? = null, + status: EnrollmentStatus? = EnrollmentStatus.ACTIVE, + ) { + helper.createEnrollment( + teiUid, + enrollmentUid, + programUid, + orgunitUid, + enrollmentDate, + incidentDate, + created, + lastUpdated, + status, + ) + } + + private fun createDefaultTrackerEvent( + eventUid: String, + enrollmentUid: String, + programUid: String = program.uid(), + programStageUid: String = programStage1.uid(), + orgunitUid: String = orgunitChild1.uid(), + deleted: Boolean = false, + eventDate: Date? = null, + created: Date? = null, + lastUpdated: Date? = null, + status: EventStatus? = EventStatus.ACTIVE, + ) { + helper.createTrackerEvent( + eventUid, + enrollmentUid, + programUid, + programStageUid, + orgunitUid, + deleted, + eventDate, + created, + lastUpdated, + status, + ) + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt index 3a467e70eb..ffb54dc7ba 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryIntegrationShould.kt @@ -29,239 +29,21 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist import com.google.common.truth.Truth.assertThat -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorIntegrationShould -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute1 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.dataElement1 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.generator -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild1 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201911 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201912 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period202001 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.program -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.programStage1 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity1 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity2 -import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntityType -import org.hisp.dhis.android.core.arch.helpers.DateUtils -import org.hisp.dhis.android.core.common.AnalyticsType -import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.hisp.dhis.android.core.event.EventStatus -import org.hisp.dhis.android.core.program.programindicatorengine.BaseTrackerDataIntegrationHelper +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher import org.hisp.dhis.android.core.utils.runner.D2JunitRunner import org.junit.Test import org.junit.runner.RunWith -import java.util.Date @RunWith(D2JunitRunner::class) -internal class TrackerLineListRepositoryIntegrationShould : BaseEvaluatorIntegrationShould() { - - private val helper = BaseTrackerDataIntegrationHelper(databaseAdapter) +internal class TrackerLineListRepositoryIntegrationShould : BaseMockIntegrationTestFullDispatcher() { @Test - fun evaluate_program_attributes() { - helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) - val enrollment1 = generator.generate() - createDefaultEnrollment(trackedEntity1.uid(), enrollment1) - helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "45") - + fun evaluate_tracker_visualization() { val result = d2.analyticsModule().trackerLineList() - .withEnrollmentOutput(program.uid()) - .withColumn(TrackerLineListItem.OrganisationUnitItem(filters = emptyList())) - .withColumn( - TrackerLineListItem.ProgramAttribute( - uid = attribute1.uid(), - filters = listOf( - DataFilter.GreaterThan("40"), - DataFilter.LowerThan("50"), - ), - ), - ) - .blockingEvaluate() - - assertThat(result.getOrThrow().rows.size).isEqualTo(1) - } - - @Test - fun evaluate_repeated_data_elements() { - helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) - val enrollment1 = generator.generate() - createDefaultEnrollment(trackedEntity1.uid(), enrollment1) - val event1 = generator.generate() - createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) - val event2 = generator.generate() - createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) - val event3 = generator.generate() - createDefaultTrackerEvent(event3, enrollment1, eventDate = period201911.startDate()) - - helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "8") - helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "19") - helper.insertTrackedEntityDataValue(event3, dataElement1.uid(), "2") - - val result1 = d2.analyticsModule().trackerLineList() - .withEnrollmentOutput(program.uid()) - .withColumn( - TrackerLineListItem.ProgramDataElement( - dataElement = dataElement1.uid(), - program = program.uid(), - programStage = programStage1.uid(), - filters = listOf( - DataFilter.GreaterThan("15"), - ), - repetitionIndexes = listOf(1, 2, 0, -1), - ), - ) - .blockingEvaluate() - - val rows = result1.getOrThrow().rows - assertThat(rows.size).isEqualTo(1) - - val row = rows.first() - assertThat(row.size).isEqualTo(4) - - rows.first().forEachIndexed { index, value -> - when (index) { - 0 -> assertThat(value.value).isEqualTo("2") - 1 -> assertThat(value.value).isEqualTo("19") - 2 -> assertThat(value.value).isEqualTo("19") - 3 -> assertThat(value.value).isEqualTo("8") - } - } - } - - @Test - fun should_filter_by_date() { - helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) - val enrollment1 = generator.generate() - createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) - helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") - - helper.createTrackedEntity(trackedEntity2.uid(), orgunitChild1.uid(), trackedEntityType.uid()) - val enrollment2 = generator.generate() - createDefaultEnrollment(trackedEntity2.uid(), enrollment2, enrollmentDate = period201912.startDate()) - helper.insertTrackedEntityAttributeValue(trackedEntity2.uid(), attribute1.uid(), "789") - - // Filter by absolute value - val result1 = d2.analyticsModule().trackerLineList() - .withEnrollmentOutput(program.uid()) - .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) - .withColumn( - TrackerLineListItem.EnrollmentDate( - filters = listOf(DateFilter.Absolute("2020")), - ), - ) - .blockingEvaluate() - - val rows1 = result1.getOrThrow().rows - assertThat(rows1.size).isEqualTo(1) - assertThat(rows1[0][0].value).isEqualTo("123") - assertThat(rows1[0][1].value).isEqualTo("2020-01-01T00:00:00.000") - - // Filter by range - val result2 = d2.analyticsModule().trackerLineList() - .withEnrollmentOutput(program.uid()) - .withColumn(TrackerLineListItem.ProgramAttribute(attribute1.uid())) - .withColumn( - TrackerLineListItem.EnrollmentDate( - filters = listOf( - DateFilter.Range( - startDate = DateUtils.DATE_FORMAT.format(period201911.startDate()!!), - endDate = DateUtils.DATE_FORMAT.format(period201912.endDate()!!), - ), - ), - ), - ) - .blockingEvaluate() - - val rows2 = result2.getOrThrow().rows - assertThat(rows2.size).isEqualTo(1) - assertThat(rows2[0][0].value).isEqualTo("789") - assertThat(rows2[0][1].value).isEqualTo("2019-12-01T00:00:00.000") - } - - @Test - fun evaluate_program_indicator() { - helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) - val enrollment1 = generator.generate() - createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) - helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute1.uid(), "123") - - val event1 = generator.generate() - createDefaultTrackerEvent(event1, enrollment1, eventDate = period202001.startDate()) - val event2 = generator.generate() - createDefaultTrackerEvent(event2, enrollment1, eventDate = period201912.startDate()) - - helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "5") - helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "10") - - val programIndicator = generator.generate() - helper.setProgramIndicatorExpression( - programIndicator, - program.uid(), - expression = "A{${attribute1.uid()}} + #{${programStage1.uid()}.${dataElement1.uid()}}", - analyticsType = AnalyticsType.EVENT, - ) - - val result = d2.analyticsModule().trackerLineList() - .withEventOutput(program.uid(), programStage1.uid()) - .withColumn(TrackerLineListItem.EventDate()) - .withColumn(TrackerLineListItem.ProgramIndicator(programIndicator)) + .withTrackerVisualization("s85urBIkN0z") .blockingEvaluate() val rows = result.getOrThrow().rows assertThat(rows.size).isEqualTo(2) - assertThat(rows[0][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period202001.startDate()!!)) - assertThat(rows[0][1].value).isEqualTo("128") - assertThat(rows[1][0].value).isEqualTo(DateUtils.DATE_FORMAT.format(period201912.startDate()!!)) - assertThat(rows[1][1].value).isEqualTo("133") - } - - private fun createDefaultEnrollment( - teiUid: String, - enrollmentUid: String, - programUid: String = program.uid(), - orgunitUid: String = orgunitChild1.uid(), - enrollmentDate: Date? = null, - incidentDate: Date? = null, - created: Date? = null, - lastUpdated: Date? = null, - status: EnrollmentStatus? = EnrollmentStatus.ACTIVE, - ) { - helper.createEnrollment( - teiUid, - enrollmentUid, - programUid, - orgunitUid, - enrollmentDate, - incidentDate, - created, - lastUpdated, - status, - ) - } - - private fun createDefaultTrackerEvent( - eventUid: String, - enrollmentUid: String, - programUid: String = program.uid(), - programStageUid: String = programStage1.uid(), - orgunitUid: String = orgunitChild1.uid(), - deleted: Boolean = false, - eventDate: Date? = null, - created: Date? = null, - lastUpdated: Date? = null, - status: EventStatus? = EventStatus.ACTIVE, - ) { - helper.createTrackerEvent( - eventUid, - enrollmentUid, - programUid, - programStageUid, - orgunitUid, - deleted, - eventDate, - created, - lastUpdated, - status, - ) } } From 1b406a440b9e4c58c8a914a073450a39fccc4788 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 18:28:42 +0100 Subject: [PATCH 144/222] [ANDROSDK-1821] Do not send orgunit parameter if mode does not allow it --- .../android/core/event/EventDownloader.kt | 8 ++--- .../TrackedEntityInstanceDownloader.kt | 12 ++++---- .../TrackedEntityInstanceQueryOnlineHelper.kt | 25 ++++++++++++---- ...edEntityInstanceQueryOnlineHelperShould.kt | 29 +++++++++++++++++++ 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/EventDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/event/EventDownloader.kt index 73a712f078..271220a49b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/EventDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/EventDownloader.kt @@ -71,18 +71,18 @@ class EventDownloader internal constructor( } fun byProgramUid(programUid: String): EventDownloader { - return cf.baseString(QueryParams.PROGRAM).eq(programUid)!! + return cf.baseString(QueryParams.PROGRAM).eq(programUid) } fun limitByOrgunit(limitByOrgunit: Boolean): EventDownloader { - return cf.bool(QueryParams.LIMIT_BY_ORGUNIT).eq(limitByOrgunit)!! + return cf.bool(QueryParams.LIMIT_BY_ORGUNIT).eq(limitByOrgunit) } fun limitByProgram(limitByProgram: Boolean): EventDownloader { - return cf.bool(QueryParams.LIMIT_BY_PROGRAM).eq(limitByProgram)!! + return cf.bool(QueryParams.LIMIT_BY_PROGRAM).eq(limitByProgram) } fun limit(limit: Int): EventDownloader { - return cf.integer(QueryParams.LIMIT).eq(limit)!! + return cf.integer(QueryParams.LIMIT).eq(limit) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloader.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloader.kt index c495746796..9d6dd81a9d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloader.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceDownloader.kt @@ -76,23 +76,23 @@ class TrackedEntityInstanceDownloader internal constructor( } fun byProgramUid(programUid: String): TrackedEntityInstanceDownloader { - return cf.baseString(QueryParams.PROGRAM).eq(programUid)!! + return cf.baseString(QueryParams.PROGRAM).eq(programUid) } fun limitByOrgunit(limitByOrgunit: Boolean): TrackedEntityInstanceDownloader { - return cf.bool(QueryParams.LIMIT_BY_ORGUNIT).eq(limitByOrgunit)!! + return cf.bool(QueryParams.LIMIT_BY_ORGUNIT).eq(limitByOrgunit) } fun limitByProgram(limitByProgram: Boolean): TrackedEntityInstanceDownloader { - return cf.bool(QueryParams.LIMIT_BY_PROGRAM).eq(limitByProgram)!! + return cf.bool(QueryParams.LIMIT_BY_PROGRAM).eq(limitByProgram) } fun limit(limit: Int): TrackedEntityInstanceDownloader { - return cf.integer(QueryParams.LIMIT).eq(limit)!! + return cf.integer(QueryParams.LIMIT).eq(limit) } fun byProgramStatus(status: EnrollmentScope): TrackedEntityInstanceDownloader { - return cf.baseString(QueryParams.PROGRAM_STATUS).eq(status.toString())!! + return cf.baseString(QueryParams.PROGRAM_STATUS).eq(status.toString()) } /** @@ -103,6 +103,6 @@ class TrackedEntityInstanceDownloader internal constructor( * @return the new repository */ fun overwrite(overwrite: Boolean): TrackedEntityInstanceDownloader { - return cf.bool(QueryParams.OVERWRITE).eq(overwrite)!! + return cf.bool(QueryParams.OVERWRITE).eq(overwrite) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt index efe36f87c5..d1be0531cf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.arch.repositories.scope.internal.RepositorySco import org.hisp.dhis.android.core.common.DateFilterPeriodHelper import org.hisp.dhis.android.core.common.FilterOperatorsHelper import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.koin.core.annotation.Singleton import java.util.Date @@ -70,12 +71,26 @@ internal class TrackedEntityInstanceQueryOnlineHelper( } } + // Integrity checks return queries.map { query -> - if (query.eventStatus == EventStatus.SCHEDULE && query.dueStartDate == null) { - query.copy(dueStartDate = Date()) - } else { - query - } + query + .run { + if (this.eventStatus == EventStatus.SCHEDULE && this.dueStartDate == null) { + copy(dueStartDate = Date()) + } else { + this + } + } + .run { + if (this.orgUnitMode == OrganisationUnitMode.ALL || + this.orgUnitMode == OrganisationUnitMode.ACCESSIBLE || + this.orgUnitMode == OrganisationUnitMode.CAPTURE + ) { + copy(orgUnits = emptyList()) + } else { + this + } + } } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt index e9e92f4c8a..6c6779d901 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt @@ -32,6 +32,7 @@ import org.hisp.dhis.android.core.arch.repositories.scope.internal.FilterItemOpe import org.hisp.dhis.android.core.arch.repositories.scope.internal.RepositoryScopeFilterItem import org.hisp.dhis.android.core.common.DateFilterPeriodHelper import org.hisp.dhis.android.core.common.FilterOperatorsHelper.listToStr +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.period.internal.CalendarProviderFactory.calendarProvider import org.hisp.dhis.android.core.period.internal.ParentPeriodGeneratorImpl.Companion.create import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnlineHelper.Companion.toAPIFilterFormat @@ -74,6 +75,34 @@ class TrackedEntityInstanceQueryOnlineHelperShould { assertThat(onlineQueries[0].attributeFilter.size).isEqualTo(1) } + @Test + fun should_empty_orgunit_list_if_mode_accessible() { + val scope = TrackedEntityInstanceQueryRepositoryScope.builder() + .orgUnits(listOf("orgunit1")) + .orgUnitMode(OrganisationUnitMode.ACCESSIBLE) + .build() + + val onlineQueries = onlineHelper.fromScope(scope) + + assertThat(onlineQueries.size).isEqualTo(1) + assertThat(onlineQueries[0].orgUnits).isEmpty() + assertThat(onlineQueries[0].orgUnitMode).isEqualTo(OrganisationUnitMode.ACCESSIBLE) + } + + @Test + fun should_empty_orgunit_list_if_mode_selected() { + val scope = TrackedEntityInstanceQueryRepositoryScope.builder() + .orgUnits(listOf("orgunit1")) + .orgUnitMode(OrganisationUnitMode.SELECTED) + .build() + + val onlineQueries = onlineHelper.fromScope(scope) + + assertThat(onlineQueries.size).isEqualTo(1) + assertThat(onlineQueries[0].orgUnits).isEqualTo(listOf("orgunit1")) + assertThat(onlineQueries[0].orgUnitMode).isEqualTo(OrganisationUnitMode.SELECTED) + } + @Test fun to_API_filter_format() { // List of filters From fcecc4abf26fe9b1878fca80db8fa4d5377fddc7 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 18:55:59 +0100 Subject: [PATCH 145/222] [ANDROSDK-1821] Do not include page if paging false --- .../NewTrackedEntityEndpointCallFactory.kt | 36 +++++++++++++------ .../TrackedEntityInstanceQueryOnlineHelper.kt | 24 +++---------- .../exporter/TrackerExporterService.kt | 8 ++--- ...edEntityInstanceQueryOnlineHelperShould.kt | 29 --------------- 4 files changed, 34 insertions(+), 63 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index df9d7892bb..d6f8639fad 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -63,7 +63,7 @@ internal class NewTrackedEntityEndpointCallFactory( return trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.allFields, trackedEntityInstances = getUidStr(query), - orgUnits = query.orgUnit, + orgUnits = getOrgunits(query), orgUnitMode = query.commonParams.ouMode.name, program = query.commonParams.program, programStatus = getProgramStatus(query), @@ -114,14 +114,14 @@ internal class NewTrackedEntityEndpointCallFactory( val instances = getTrackedEntityQuery(teiQuery) TrackerQueryResult( trackedEntities = instances, - exhausted = events.size < query.pageSize, + exhausted = events.size < query.pageSize || !query.paging, ) } } else { val instances = getTrackedEntityQuery(query) TrackerQueryResult( trackedEntities = instances, - exhausted = instances.size < query.pageSize, + exhausted = instances.size < query.pageSize || !query.paging, ) } } @@ -163,8 +163,8 @@ internal class NewTrackedEntityEndpointCallFactory( order = toAPIOrderFormat(query.order, TrackerExporterVersion.V2), assignedUserMode = query.assignedUserMode?.toString(), paging = query.paging, - pageSize = query.pageSize, - page = query.page, + pageSize = query.pageSize.takeIf { query.paging }, + page = query.page.takeIf { query.paging }, updatedAfter = query.lastUpdatedStartDate.simpleDateFormat(), updatedBefore = query.lastUpdatedEndDate.simpleDateFormat(), includeDeleted = query.includeDeleted, @@ -181,7 +181,7 @@ internal class NewTrackedEntityEndpointCallFactory( val payload = trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.asRelationshipFields, trackedEntityInstances = uidsStr, - orgUnits = getOrgunits(query.orgUnits), + orgUnits = getOrgunits(query), orgUnitMode = query.orgUnitMode?.toString(), program = query.program, programStage = query.programStage, @@ -201,8 +201,8 @@ internal class NewTrackedEntityEndpointCallFactory( lastUpdatedEndDate = query.lastUpdatedEndDate.simpleDateFormat(), order = toAPIOrderFormat(query.order, TrackerExporterVersion.V2), paging = query.paging, - page = query.page, - pageSize = query.pageSize, + page = query.page.takeIf { query.paging }, + pageSize = query.pageSize.takeIf { query.paging }, ) mapPayload(payload) @@ -232,11 +232,25 @@ internal class NewTrackedEntityEndpointCallFactory( return Payload(newItems) } - private fun getOrgunits(orgUnits: List): String? { - return if (orgUnits.isEmpty()) { + private fun getOrgunits(query: TrackerAPIQuery): String? { + return getOrgunits(query.orgUnit?.let { listOf(it) }, query.commonParams.ouMode) + } + + private fun getOrgunits(query: TrackedEntityInstanceQueryOnline): String? { + return getOrgunits(query.orgUnits, query.orgUnitMode) + } + + private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): String? { + return if (orgunits.isNullOrEmpty()) { + null + } else if ( + mode == OrganisationUnitMode.ALL || + mode == OrganisationUnitMode.ACCESSIBLE || + mode == OrganisationUnitMode.CAPTURE + ) { null } else { - orgUnits.joinToString(";") + orgunits.joinToString(";") } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt index d1be0531cf..e8e9cef0d1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt @@ -71,26 +71,12 @@ internal class TrackedEntityInstanceQueryOnlineHelper( } } - // Integrity checks return queries.map { query -> - query - .run { - if (this.eventStatus == EventStatus.SCHEDULE && this.dueStartDate == null) { - copy(dueStartDate = Date()) - } else { - this - } - } - .run { - if (this.orgUnitMode == OrganisationUnitMode.ALL || - this.orgUnitMode == OrganisationUnitMode.ACCESSIBLE || - this.orgUnitMode == OrganisationUnitMode.CAPTURE - ) { - copy(orgUnits = emptyList()) - } else { - this - } - } + if (query.eventStatus == EventStatus.SCHEDULE && query.dueStartDate == null) { + query.copy(dueStartDate = Date()) + } else { + query + } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index 3edbc71c88..dab7ae63d4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -72,8 +72,8 @@ internal interface TrackerExporterService { @Query(UPDATED_BEFORE) lastUpdatedEndDate: String? = null, @Query(ORDER) order: String? = null, @Query(PAGING) paging: Boolean, - @Query(PAGE) page: Int, - @Query(PAGE_SIZE) pageSize: Int, + @Query(PAGE) page: Int?, + @Query(PAGE_SIZE) pageSize: Int?, @Query(INCLUDE_DELETED) includeDeleted: Boolean = false, ): TrackerPayload @@ -106,8 +106,8 @@ internal interface TrackerExporterService { @Query(ORDER) order: String? = null, @Query(ASSIGNED_USER_MODE) assignedUserMode: String? = null, @Query(PAGING) paging: Boolean, - @Query(PAGE) page: Int, - @Query(PAGE_SIZE) pageSize: Int, + @Query(PAGE) page: Int?, + @Query(PAGE_SIZE) pageSize: Int?, @Query(UPDATED_AFTER) updatedAfter: String?, @Query(UPDATED_BEFORE) updatedBefore: String? = null, @Query(INCLUDE_DELETED) includeDeleted: Boolean, diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt index 6c6779d901..e9e92f4c8a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt @@ -32,7 +32,6 @@ import org.hisp.dhis.android.core.arch.repositories.scope.internal.FilterItemOpe import org.hisp.dhis.android.core.arch.repositories.scope.internal.RepositoryScopeFilterItem import org.hisp.dhis.android.core.common.DateFilterPeriodHelper import org.hisp.dhis.android.core.common.FilterOperatorsHelper.listToStr -import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.period.internal.CalendarProviderFactory.calendarProvider import org.hisp.dhis.android.core.period.internal.ParentPeriodGeneratorImpl.Companion.create import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnlineHelper.Companion.toAPIFilterFormat @@ -75,34 +74,6 @@ class TrackedEntityInstanceQueryOnlineHelperShould { assertThat(onlineQueries[0].attributeFilter.size).isEqualTo(1) } - @Test - fun should_empty_orgunit_list_if_mode_accessible() { - val scope = TrackedEntityInstanceQueryRepositoryScope.builder() - .orgUnits(listOf("orgunit1")) - .orgUnitMode(OrganisationUnitMode.ACCESSIBLE) - .build() - - val onlineQueries = onlineHelper.fromScope(scope) - - assertThat(onlineQueries.size).isEqualTo(1) - assertThat(onlineQueries[0].orgUnits).isEmpty() - assertThat(onlineQueries[0].orgUnitMode).isEqualTo(OrganisationUnitMode.ACCESSIBLE) - } - - @Test - fun should_empty_orgunit_list_if_mode_selected() { - val scope = TrackedEntityInstanceQueryRepositoryScope.builder() - .orgUnits(listOf("orgunit1")) - .orgUnitMode(OrganisationUnitMode.SELECTED) - .build() - - val onlineQueries = onlineHelper.fromScope(scope) - - assertThat(onlineQueries.size).isEqualTo(1) - assertThat(onlineQueries[0].orgUnits).isEqualTo(listOf("orgunit1")) - assertThat(onlineQueries[0].orgUnitMode).isEqualTo(OrganisationUnitMode.SELECTED) - } - @Test fun to_API_filter_format() { // List of filters From c0aaa5b6491d70e7ad38a14236a7bf5374be2616 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 19:13:24 +0100 Subject: [PATCH 146/222] [ANDROSDK-1821] Do not include page if paging false (old exporter) --- .../NewTrackedEntityEndpointCallFactory.kt | 23 +------- .../OldTrackedEntityEndpointCallFactory.kt | 3 +- .../internal/TrackedEntityInstanceService.kt | 8 +-- .../TrackedEntityInstanceQueryCallFactory.kt | 15 ++--- .../TrackedEntityInstanceQueryOnlineHelper.kt | 1 - .../tracker/exporter/TrackerQueryHelper.kt | 56 +++++++++++++++++++ 6 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index d6f8639fad..a44bb24e5b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -48,6 +48,7 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterService +import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton @@ -232,28 +233,6 @@ internal class NewTrackedEntityEndpointCallFactory( return Payload(newItems) } - private fun getOrgunits(query: TrackerAPIQuery): String? { - return getOrgunits(query.orgUnit?.let { listOf(it) }, query.commonParams.ouMode) - } - - private fun getOrgunits(query: TrackedEntityInstanceQueryOnline): String? { - return getOrgunits(query.orgUnits, query.orgUnitMode) - } - - private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): String? { - return if (orgunits.isNullOrEmpty()) { - null - } else if ( - mode == OrganisationUnitMode.ALL || - mode == OrganisationUnitMode.ACCESSIBLE || - mode == OrganisationUnitMode.CAPTURE - ) { - null - } else { - orgunits.joinToString(";") - } - } - private fun getRelatedProgramUid(item: RelationshipItemRelative): String? { val relationshipType = relationshipTypeRepository .withConstraints() diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt index bc5ad5cffe..1e9ee078a8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt @@ -37,6 +37,7 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQuer import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery +import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.koin.core.annotation.Singleton @Singleton @@ -49,7 +50,7 @@ internal class OldTrackedEntityEndpointCallFactory( return trackedEntityInstanceService.getTrackedEntityInstances( fields = TrackedEntityInstanceFields.allFields, trackedEntityInstances = getUidStr(query), - orgUnits = query.orgUnit, + orgUnits = getOrgunits(query), orgUnitMode = query.commonParams.ouMode.name, program = query.commonParams.program, programStatus = getProgramStatus(query), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt index 11d3d01a62..860d6daf69 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceService.kt @@ -75,8 +75,8 @@ internal interface TrackedEntityInstanceService { @Query(FIELDS) @Which fields: Fields, @Query(ORDER) order: String?, @Query(PAGING) paging: Boolean, - @Query(PAGE) page: Int, - @Query(PAGE_SIZE) pageSize: Int, + @Query(PAGE) page: Int?, + @Query(PAGE_SIZE) pageSize: Int?, @Query(LAST_UPDATED_START_DATE) lastUpdatedStartDate: String?, @Query(INCLUDE_ALL_ATTRIBUTES) includeAllAttributes: Boolean, @Query(INCLUDE_DELETED) includeDeleted: Boolean, @@ -105,8 +105,8 @@ internal interface TrackedEntityInstanceService { @Query(LAST_UPDATED_END_DATE) lastUpdatedEndDate: String?, @Query(ORDER) order: String?, @Query(PAGING) paging: Boolean, - @Query(PAGE) page: Int, - @Query(PAGE_SIZE) pageSize: Int, + @Query(PAGE) page: Int?, + @Query(PAGE_SIZE) pageSize: Int?, ): SearchGrid companion object { diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt index f01f4e1ab5..91b86221c2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt @@ -43,6 +43,7 @@ import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityInstanceSe import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnlineHelper.Companion.toAPIFilterFormat import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnlineHelper.Companion.toAPIOrderFormat import org.hisp.dhis.android.core.tracker.TrackerExporterVersion +import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.hisp.dhis.android.core.util.simpleDateFormat import org.koin.core.annotation.Singleton import java.text.ParseException @@ -135,7 +136,7 @@ internal class TrackedEntityInstanceQueryCallFactory( ) { trackedEntityService.query( trackedEntityInstance = uidsStr, - orgUnit = getOrgunits(query.orgUnits), + orgUnit = getOrgunits(query), orgUnitMode = query.orgUnitMode?.toString(), program = query.program, programStage = query.programStage, @@ -155,8 +156,8 @@ internal class TrackedEntityInstanceQueryCallFactory( lastUpdatedEndDate = query.lastUpdatedEndDate.simpleDateFormat(), order = toAPIOrderFormat(query.order, TrackerExporterVersion.V1), paging = query.paging, - pageSize = query.pageSize, - page = query.page, + pageSize = query.pageSize.takeIf { query.paging }, + page = query.page.takeIf { query.paging }, ) }.getOrThrow().let { mapper.transform(it) } } catch (pe: ParseException) { @@ -179,14 +180,6 @@ internal class TrackedEntityInstanceQueryCallFactory( } } - private fun getOrgunits(orgUnits: List): String? { - return if (orgUnits.isEmpty()) { - null - } else { - orgUnits.joinToString(";") - } - } - companion object { internal fun getPostEventTeiQuery( query: TrackedEntityInstanceQueryOnline, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt index e8e9cef0d1..efe36f87c5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt @@ -33,7 +33,6 @@ import org.hisp.dhis.android.core.arch.repositories.scope.internal.RepositorySco import org.hisp.dhis.android.core.common.DateFilterPeriodHelper import org.hisp.dhis.android.core.common.FilterOperatorsHelper import org.hisp.dhis.android.core.event.EventStatus -import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.koin.core.annotation.Singleton import java.util.Date diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt new file mode 100644 index 0000000000..4d341c0b02 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.tracker.exporter + +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline + +internal object TrackerQueryHelper { + fun getOrgunits(query: TrackerAPIQuery): String? { + return getOrgunits(query.orgUnit?.let { listOf(it) }, query.commonParams.ouMode) + } + + fun getOrgunits(query: TrackedEntityInstanceQueryOnline): String? { + return getOrgunits(query.orgUnits, query.orgUnitMode) + } + + private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): String? { + return if (orgunits.isNullOrEmpty()) { + null + } else if ( + mode == OrganisationUnitMode.ALL || + mode == OrganisationUnitMode.ACCESSIBLE || + mode == OrganisationUnitMode.CAPTURE + ) { + null + } else { + orgunits.joinToString(";") + } + } +} From 15e5c592f532d9dab853cea6025bed5a86e94d16 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 29 Feb 2024 08:41:32 +0100 Subject: [PATCH 147/222] [ANDROSDK-1809] Add integration test for single events --- ...rackerLineListRepositoryEvaluatorShould.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 188c9901cb..164a00ee6e 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -215,6 +215,26 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati assertThat(rows[1][1].value).isEqualTo("133") } + @Test + fun evaluate_single_events() { + val event1 = generator.generate() + helper.createSingleEvent(event1, program.uid(), programStage1.uid(), orgunitChild1.uid()) + val event2 = generator.generate() + helper.createSingleEvent(event2, program.uid(), programStage1.uid(), orgunitChild1.uid()) + + helper.insertTrackedEntityDataValue(event1, dataElement1.uid(), "5") + helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "10") + + val result = d2.analyticsModule().trackerLineList() + .withEventOutput(program.uid(), programStage1.uid()) + .withColumn(TrackerLineListItem.ProgramDataElement(dataElement1.uid(), program.uid(), programStage1.uid())) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(2) + assertThat(rows.flatten().map { it.value }).containsExactly("5", "10") + } + private fun createDefaultEnrollment( teiUid: String, enrollmentUid: String, From 2db46d826e03debc9f9da26ef3f550a89875b4d5 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 29 Feb 2024 08:53:02 +0100 Subject: [PATCH 148/222] [ANDROSDK-1821] Fix query parameters for events (old tracker) --- .../hisp/dhis/android/core/event/internal/EventService.kt | 4 ++-- .../core/event/internal/OldEventEndpointCallFactory.kt | 3 ++- .../search/TrackedEntityInstanceQueryCallFactory.kt | 6 +++--- .../android/core/tracker/exporter/TrackerQueryHelper.kt | 4 ++++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventService.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventService.kt index ff661be3f6..1e57e8792f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventService.kt @@ -58,8 +58,8 @@ internal interface EventService { @Query(ORDER) order: String? = null, @Query(ASSIGNED_USER_MODE) assignedUserMode: String? = null, @Query(PAGING) paging: Boolean, - @Query(PAGE) page: Int, - @Query(PAGE_SIZE) pageSize: Int, + @Query(PAGE) page: Int?, + @Query(PAGE_SIZE) pageSize: Int?, @Query(LAST_UPDATED_START_DATE) lastUpdatedStartDate: String? = null, @Query(LAST_UPDATED_END_DATE) lastUpdatedEndDate: String? = null, @Query(INCLUDE_DELETED) includeDeleted: Boolean, diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt index efa98b269d..d4245ef718 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt @@ -32,6 +32,7 @@ import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery +import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.koin.core.annotation.Singleton @Singleton @@ -42,7 +43,7 @@ internal class OldEventEndpointCallFactory( override suspend fun getCollectionCall(eventQuery: TrackerAPIQuery): Payload { return service.getEvents( fields = EventFields.allFields, - orgUnit = eventQuery.orgUnit, + orgUnit = getOrgunits(eventQuery), orgUnitMode = eventQuery.commonParams.ouMode.name, program = eventQuery.commonParams.program, startDate = getEventStartDate(eventQuery), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt index 91b86221c2..d31a822806 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt @@ -102,7 +102,7 @@ internal class TrackedEntityInstanceQueryCallFactory( return coroutineAPICallExecutor.wrap(storeError = false) { eventService.getEvents( fields = EventFields.teiQueryFields, - orgUnit = orgunit, + orgUnit = getOrgunits(orgunit, query.orgUnitMode), orgUnitMode = query.orgUnitMode?.toString(), status = query.eventStatus?.toString(), program = query.program, @@ -117,8 +117,8 @@ internal class TrackedEntityInstanceQueryCallFactory( order = toAPIOrderFormat(query.order, TrackerExporterVersion.V1), assignedUserMode = query.assignedUserMode?.toString(), paging = query.paging, - pageSize = query.pageSize, - page = query.page, + pageSize = query.pageSize.takeIf { query.paging }, + page = query.page.takeIf { query.paging }, lastUpdatedStartDate = query.lastUpdatedStartDate.simpleDateFormat(), lastUpdatedEndDate = query.lastUpdatedEndDate.simpleDateFormat(), includeDeleted = query.includeDeleted, diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt index 4d341c0b02..19d164960e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt @@ -40,6 +40,10 @@ internal object TrackerQueryHelper { return getOrgunits(query.orgUnits, query.orgUnitMode) } + fun getOrgunits(orgunit: String?, mode: OrganisationUnitMode?): String? { + return getOrgunits(orgunit?.let { listOf(it) }, mode) + } + private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): String? { return if (orgunits.isNullOrEmpty()) { null From 78d5bfedf4cf52f29abc107c9ef88c703cc6f74f Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 29 Feb 2024 09:53:12 +0100 Subject: [PATCH 149/222] [ANDROSDK-1822] Adapt to changes in tracker API http status code --- .../TrackedEntityInstanceCallErrorCatcher.kt | 7 +++++- .../TrackedEntityInstanceQueryErrorCatcher.kt | 6 ++++- ...edEntityInstanceQueryErrorCatcherShould.kt | 25 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallErrorCatcher.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallErrorCatcher.kt index a7db2d45ea..b47c12f421 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallErrorCatcher.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityInstanceCallErrorCatcher.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.imports.internal.HttpMessageResponse import org.hisp.dhis.android.core.maintenance.D2ErrorCode import retrofit2.Response import java.io.IOException +import javax.net.ssl.HttpsURLConnection import kotlin.Throws internal class TrackedEntityInstanceCallErrorCatcher : APICallErrorCatcher { @@ -45,7 +46,11 @@ internal class TrackedEntityInstanceCallErrorCatcher : APICallErrorCatcher { val parsed = objectMapper().readValue(errorBody, HttpMessageResponse::class.java) @Suppress("MagicNumber") - return if (parsed.httpStatusCode() == 401) { + return if ( + parsed.httpStatusCode() == HttpsURLConnection.HTTP_UNAUTHORIZED || + parsed.httpStatusCode() == HttpsURLConnection.HTTP_CONFLICT || + parsed.httpStatusCode() == HttpsURLConnection.HTTP_FORBIDDEN + ) { when (parsed.message()) { "OWNERSHIP_ACCESS_DENIED" -> D2ErrorCode.OWNERSHIP_ACCESS_DENIED "PROGRAM_ACCESS_CLOSED" -> D2ErrorCode.PROGRAM_ACCESS_CLOSED diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcher.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcher.kt index 23a1640440..678865aff8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcher.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcher.kt @@ -59,7 +59,11 @@ internal class TrackedEntityInstanceQueryErrorCatcher : APICallErrorCatcher { private fun parseErrorMessage(errorBody: String): D2ErrorCode { return try { val parsed = objectMapper().readValue(errorBody, HttpMessageResponse::class.java) - if (parsed.httpStatusCode() == HttpsURLConnection.HTTP_CONFLICT) { + if ( + parsed.httpStatusCode() == HttpsURLConnection.HTTP_UNAUTHORIZED || + parsed.httpStatusCode() == HttpsURLConnection.HTTP_CONFLICT || + parsed.httpStatusCode() == HttpsURLConnection.HTTP_FORBIDDEN + ) { when { parsed.message() == "maxteicountreached" -> D2ErrorCode.MAX_TEI_COUNT_REACHED OutOfSearchScope.containsMatchIn(parsed.message()) -> D2ErrorCode.ORGUNIT_NOT_IN_SEARCH_SCOPE diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt index b815367080..af10fd9e43 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.trackedentity.search import com.google.common.truth.Truth.assertThat import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.junit.Test import org.junit.runner.RunWith @@ -43,7 +44,7 @@ class TrackedEntityInstanceQueryErrorCatcherShould { @Test fun return_too_many_orgunits() { val response = - Response.error(HttpsURLConnection.HTTP_REQ_TOO_LONG, ResponseBody.create(null, "")) + Response.error(HttpsURLConnection.HTTP_REQ_TOO_LONG, "".toResponseBody(null)) assertThat(catcher.catchError(response, response.errorBody()!!.string())) .isEqualTo(D2ErrorCode.TOO_MANY_ORG_UNITS) @@ -59,7 +60,7 @@ class TrackedEntityInstanceQueryErrorCatcherShould { "message": "maxteicountreached" }""" - val response = Response.error(409, ResponseBody.create(null, responseError)) + val response = Response.error(409, responseError.toResponseBody(null)) assertThat(catcher.catchError(response, response.errorBody()!!.string())) .isEqualTo(D2ErrorCode.MAX_TEI_COUNT_REACHED) } @@ -74,7 +75,23 @@ class TrackedEntityInstanceQueryErrorCatcherShould { "message": "Organisation unit is not part of the search scope: O6uvpzGd5pu" }""" - val response = Response.error(409, ResponseBody.create(null, responseError)) + val response = Response.error(409, responseError.toResponseBody(null)) + assertThat(catcher.catchError(response, response.errorBody()!!.string())) + .isEqualTo(D2ErrorCode.ORGUNIT_NOT_IN_SEARCH_SCOPE) + } + + @Test + fun return_orgunit_not_in_search_scope_v41() { + val responseError = + """{ + "httpStatus": "Forbidden", + "httpStatusCode": 403, + "status": "ERROR", + "message": "Organisation unit is not part of the search scope: vWbkYPRmKyS", + "errorCode": "E1006" + }""" + + val response = Response.error(403, responseError.toResponseBody(null)) assertThat(catcher.catchError(response, response.errorBody()!!.string())) .isEqualTo(D2ErrorCode.ORGUNIT_NOT_IN_SEARCH_SCOPE) } @@ -89,7 +106,7 @@ class TrackedEntityInstanceQueryErrorCatcherShould { "message": "At least 1 attributes should be mentioned in the search criteria." }""" - val response = Response.error(409, ResponseBody.create(null, responseError)) + val response = Response.error(409, responseError.toResponseBody(null)) assertThat(catcher.catchError(response, response.errorBody()!!.string())) .isEqualTo(D2ErrorCode.MIN_SEARCH_ATTRIBUTES_REQUIRED) } From 25cb8f490f35b71efdb8575bc09440281bff6fc7 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 29 Feb 2024 10:15:55 +0100 Subject: [PATCH 150/222] [ANDROSDK-1821] Adapt unit test --- .../TrackedEntityInstanceQueryCallShould.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt index 8e67307730..c84e22e7a3 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallShould.kt @@ -91,7 +91,7 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { query = TrackedEntityInstanceQueryOnline( uids = listOf("uid1", "uid2"), orgUnits = orgUnits, - orgUnitMode = OrganisationUnitMode.ACCESSIBLE, + orgUnitMode = OrganisationUnitMode.DESCENDANTS, program = "program", programStartDate = Date(), programEndDate = Date(), @@ -232,7 +232,7 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { ) = runBlocking { verify(trackedEntityService).query( eq(query.uids?.joinToString(";")), - eq(query.orgUnits.joinToString(";")), + getExpectedOrgunit(query.orgUnits, query.orgUnitMode), eq(query.orgUnitMode.toString()), eq(query.program), eq(query.programStage), @@ -252,8 +252,8 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { eq(query.lastUpdatedEndDate.simpleDateFormat()), any(), eq(query.paging), - eq(query.page), - eq(query.pageSize), + eq(query.page.takeIf { query.paging }), + eq(query.pageSize.takeIf { query.paging }), ) } @@ -272,7 +272,7 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { private fun verifyEventServiceForOrgunit(query: TrackedEntityInstanceQueryOnline, orgunit: String?) = runBlocking { verify(eventService).getEvents( eq(EventFields.teiQueryFields), - eq(orgunit), + getExpectedOrgunit(orgunit?.let { listOf(it) }, query.orgUnitMode), eq(query.orgUnitMode?.toString()), eq(query.eventStatus?.toString()), eq(query.program), @@ -287,8 +287,8 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { any(), eq(query.assignedUserMode?.toString()), eq(query.paging), - eq(query.page), - eq(query.pageSize), + eq(query.page.takeIf { query.paging }), + eq(query.pageSize.takeIf { query.paging }), eq(query.lastUpdatedStartDate.simpleDateFormat()), eq(query.lastUpdatedEndDate.simpleDateFormat()), eq(query.includeDeleted), @@ -296,6 +296,17 @@ class TrackedEntityInstanceQueryCallShould : BaseCallShould() { ) } + private fun getExpectedOrgunit(orgUnits: List?, mode: OrganisationUnitMode?): String? { + return if (mode == OrganisationUnitMode.ALL || + mode == OrganisationUnitMode.ACCESSIBLE || + mode == OrganisationUnitMode.CAPTURE + ) { + eq(null) + } else { + eq(orgUnits?.joinToString(";")) + } + } + private fun whenServiceQuery(answer: (InvocationOnMock) -> SearchGrid?) { trackedEntityService.stub { onBlocking { From 79c4d817b01e7378cc20bad93e5c2f366ee5a4ae Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 29 Feb 2024 10:25:52 +0100 Subject: [PATCH 151/222] [ANDROSDK-1822] Remove unused import --- .../search/TrackedEntityInstanceQueryErrorCatcherShould.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt index af10fd9e43..997d182283 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryErrorCatcherShould.kt @@ -28,7 +28,6 @@ package org.hisp.dhis.android.core.trackedentity.search import com.google.common.truth.Truth.assertThat -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.junit.Test From c242e61ef4cf8a11d59700a55dc68ef4e60f021d Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Fri, 1 Mar 2024 11:55:02 +0100 Subject: [PATCH 152/222] [ANDROSDK-1750] Add more tests --- .../core/systeminfo/DHISVersionManager.kt | 2 -- .../internal/DHISVersionManagerImpl.kt | 1 - .../android/core/systeminfo/SMSVersionShould.kt | 8 +++++++- .../internal/DHISVersionManagerShould.kt | 5 ++++- .../systeminfo/internal/DHISVersionShould.kt | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt index 4c681eeb89..d569ed5e69 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/DHISVersionManager.kt @@ -27,8 +27,6 @@ */ package org.hisp.dhis.android.core.systeminfo -import androidx.annotation.VisibleForTesting - interface DHISVersionManager { fun getVersion(): DHISVersion fun getPatchVersion(): DHISPatchVersion? diff --git a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt index 7a4dce9803..59ce60ca5a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerImpl.kt @@ -27,7 +27,6 @@ */ package org.hisp.dhis.android.core.systeminfo.internal -import androidx.annotation.VisibleForTesting import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.maintenance.D2ErrorComponent diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt index 35e0d3b200..4a4e2d7045 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/SMSVersionShould.kt @@ -59,10 +59,16 @@ class SMSVersionShould { @Test fun return_unknown_version_if_patch_does_not_exist_but_bypass_is_true() { - val smsVersion = getValue("2.32.100", true) + val smsVersion = getValue("2.100.100", true) assertThat(smsVersion).isEqualTo(DHISPatchVersion.UNKNOWN.smsVersion) } + @Test + fun return_null_if_patch_does_not_exist_but_bypass_is_false() { + val smsVersion = getValue("2.100.100", false) + assertThat(smsVersion).isNull() + } + @Test fun return_non_null_for_any_version_greater_than_2_32() { DHISVersion.entries diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt index d0b98b61b1..665f043956 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionManagerShould.kt @@ -113,7 +113,10 @@ class DHISVersionManagerShould { @Test fun return_unknown_if_unknown_patch_version_and_bypass_dhis2_version_is_true() { dhisVersionManager.setBypassVersion(true) - whenever(systemInfo.version()).thenReturn("2.47.59") + whenever(systemInfo.version()).thenReturn("2.100.100") + assertThat(dhisVersionManager.getPatchVersion()).isEqualTo(DHISPatchVersion.UNKNOWN) + + whenever(systemInfo.version()).thenReturn("2.100.0") assertThat(dhisVersionManager.getPatchVersion()).isEqualTo(DHISPatchVersion.UNKNOWN) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt index d6046b22f4..264609a055 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/systeminfo/internal/DHISVersionShould.kt @@ -46,4 +46,20 @@ class DHISVersionShould { } } } + + @Test + fun return_unknown_when_bypassing_version_for_unsupported_versions() { + DHISVersion.entries + .forEach { + if (it.supported) { + assertThat(DHISVersion.getValue(it.prefix + ".0", true)).isNotNull() + assertThat(DHISVersion.getValue(it.prefix + ".9", true)).isNotNull() + } else { + assertThat(DHISVersion.getValue(it.prefix + ".0", true)) + .isEqualTo(DHISVersion.UNKNOWN) + assertThat(DHISVersion.getValue(it.prefix + ".9", true)) + .isEqualTo(DHISVersion.UNKNOWN) + } + } + } } From b190031cadf43675b3321f0e58a496a545d6de04 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 1 Mar 2024 12:12:15 +0100 Subject: [PATCH 153/222] [ANDROSDK-1823] Adapt /api/tracker parameters --- .../internal/NewEventEndpointCallFactory.kt | 10 ++- .../internal/OldEventEndpointCallFactory.kt | 2 +- .../NewTrackedEntityEndpointCallFactory.kt | 22 +++--- .../OldTrackedEntityEndpointCallFactory.kt | 3 +- .../TrackedEntityEndpointCallFactory.kt | 7 +- .../TrackedEntityInstanceQueryCallFactory.kt | 8 +- .../TrackerExporterParameterManager.kt | 78 +++++++++++++++++++ .../exporter/TrackerExporterService.kt | 46 ++++++----- .../tracker/exporter/TrackerQueryHelper.kt | 10 +-- 9 files changed, 136 insertions(+), 50 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt index fb87a4ec06..3780b27479 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt @@ -35,19 +35,21 @@ import org.hisp.dhis.android.core.event.NewTrackerImporterEventTransformer import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery +import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterParameterManager import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterService import org.koin.core.annotation.Singleton @Singleton internal class NewEventEndpointCallFactory( private val service: TrackerExporterService, + private val parameterManager: TrackerExporterParameterManager, ) : EventEndpointCallFactory() { override suspend fun getCollectionCall(eventQuery: TrackerAPIQuery): Payload { return service.getEvents( fields = NewEventFields.allFields, orgUnit = eventQuery.orgUnit, - orgUnitMode = eventQuery.commonParams.ouMode.name, + orgUnitMode = parameterManager.getOrgunitModeParameter(eventQuery.commonParams.ouMode), program = eventQuery.commonParams.program, occurredAfter = getEventStartDate(eventQuery), paging = true, @@ -55,15 +57,15 @@ internal class NewEventEndpointCallFactory( pageSize = eventQuery.pageSize, updatedAfter = eventQuery.lastUpdatedStr, includeDeleted = true, - eventUid = getUidStr(eventQuery), + eventUid = parameterManager.getEventParameter(eventQuery.uids), ).let { mapPayload(it) } } override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { return service.getEventSingle( - eventUid = item.itemUid, + eventUid = parameterManager.getEventParameter(listOf(item.itemUid)), fields = NewEventFields.asRelationshipFields, - orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, + orgUnitMode = parameterManager.getOrgunitModeParameter(OrganisationUnitMode.ACCESSIBLE), ).let { mapPayload(it) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt index d4245ef718..116c82eefd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/OldEventEndpointCallFactory.kt @@ -43,7 +43,7 @@ internal class OldEventEndpointCallFactory( override suspend fun getCollectionCall(eventQuery: TrackerAPIQuery): Payload { return service.getEvents( fields = EventFields.allFields, - orgUnit = getOrgunits(eventQuery), + orgUnit = getOrgunits(eventQuery)?.firstOrNull(), orgUnitMode = eventQuery.commonParams.ouMode.name, program = eventQuery.commonParams.program, startDate = getEventStartDate(eventQuery), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index a44bb24e5b..7ffd597867 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -47,6 +47,7 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQuer import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery +import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterParameterManager import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterService import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.hisp.dhis.android.core.util.simpleDateFormat @@ -58,14 +59,15 @@ internal class NewTrackedEntityEndpointCallFactory( private val trackedExporterService: TrackerExporterService, private val coroutineAPICallExecutor: CoroutineAPICallExecutor, private val relationshipTypeRepository: RelationshipTypeCollectionRepository, + private val parameterManager: TrackerExporterParameterManager, ) : TrackedEntityEndpointCallFactory() { override suspend fun getCollectionCall(query: TrackerAPIQuery): Payload { return trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.allFields, - trackedEntityInstances = getUidStr(query), - orgUnits = getOrgunits(query), - orgUnitMode = query.commonParams.ouMode.name, + trackedEntityInstances = parameterManager.getTrackedEntityParameter(query.uids), + orgUnits = parameterManager.getOrgunitsParameter(getOrgunits(query)), + orgUnitMode = parameterManager.getOrgunitModeParameter(query.commonParams.ouMode), program = query.commonParams.program, programStatus = getProgramStatus(query), programStartDate = getProgramStartDate(query), @@ -82,7 +84,7 @@ internal class NewTrackedEntityEndpointCallFactory( return trackedExporterService.getSingleTrackedEntityInstance( fields = NewTrackedEntityInstanceFields.allFields, trackedEntityInstanceUid = uid, - orgUnitMode = query.commonParams.ouMode.name, + orgUnitMode = parameterManager.getOrgunitModeParameter(query.commonParams.ouMode), program = query.commonParams.program, programStatus = getProgramStatus(query), programStartDate = getProgramStartDate(query), @@ -94,7 +96,7 @@ internal class NewTrackedEntityEndpointCallFactory( return trackedExporterService.getSingleTrackedEntityInstance( fields = NewTrackedEntityInstanceFields.asRelationshipFields, trackedEntityInstanceUid = item.itemUid, - orgUnitMode = OrganisationUnitMode.ACCESSIBLE.name, + orgUnitMode = parameterManager.getOrgunitModeParameter(OrganisationUnitMode.ACCESSIBLE), program = getRelatedProgramUid(item), programStatus = null, programStartDate = null, @@ -145,7 +147,7 @@ internal class NewTrackedEntityEndpointCallFactory( trackedExporterService.getEvents( fields = NewEventFields.teiQueryFields, orgUnit = orgunit, - orgUnitMode = query.orgUnitMode?.toString(), + orgUnitMode = parameterManager.getOrgunitModeParameter(query.orgUnitMode), status = query.eventStatus?.toString(), program = query.program, programStage = query.programStage, @@ -177,13 +179,11 @@ internal class NewTrackedEntityEndpointCallFactory( return coroutineAPICallExecutor.wrap( errorCatcher = TrackedEntityInstanceQueryErrorCatcher(), ) { - val uidsStr = query.uids?.joinToString(";") - val payload = trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.asRelationshipFields, - trackedEntityInstances = uidsStr, - orgUnits = getOrgunits(query), - orgUnitMode = query.orgUnitMode?.toString(), + trackedEntityInstances = parameterManager.getTrackedEntityParameter(query.uids), + orgUnits = parameterManager.getOrgunitsParameter(getOrgunits(query)), + orgUnitMode = parameterManager.getOrgunitModeParameter(query.orgUnitMode), program = query.program, programStage = query.programStage, programStartDate = query.programStartDate.simpleDateFormat(), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt index 1e9ee078a8..bdf22cde87 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackedEntityEndpointCallFactory.kt @@ -37,7 +37,6 @@ import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQuer import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult import org.hisp.dhis.android.core.tracker.TrackerExporterVersion import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery -import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper.getOrgunits import org.koin.core.annotation.Singleton @Singleton @@ -50,7 +49,7 @@ internal class OldTrackedEntityEndpointCallFactory( return trackedEntityInstanceService.getTrackedEntityInstances( fields = TrackedEntityInstanceFields.allFields, trackedEntityInstances = getUidStr(query), - orgUnits = getOrgunits(query), + orgUnits = getOrgunitStr(query), orgUnitMode = query.commonParams.ouMode.name, program = query.commonParams.program, programStatus = getProgramStatus(query), diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt index 949ecb8da5..09edfb94dd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline import org.hisp.dhis.android.core.trackedentity.search.TrackerQueryResult import org.hisp.dhis.android.core.tracker.exporter.TrackerAPIQuery +import org.hisp.dhis.android.core.tracker.exporter.TrackerQueryHelper internal abstract class TrackedEntityEndpointCallFactory { @@ -46,7 +47,11 @@ internal abstract class TrackedEntityEndpointCallFactory { abstract suspend fun getQueryCall(query: TrackedEntityInstanceQueryOnline): TrackerQueryResult protected fun getUidStr(query: TrackerAPIQuery): String? { - return if (query.uids.isEmpty()) null else CollectionsHelper.joinCollectionWithSeparator(query.uids, ";") + return if (query.uids.isEmpty()) null else query.uids.joinToString(";") + } + + protected fun getOrgunitStr(query: TrackerAPIQuery): String? { + return TrackerQueryHelper.getOrgunits(query)?.joinToString(";") } protected fun getProgramStatus(query: TrackerAPIQuery): String? { diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt index d31a822806..83e1aba551 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryCallFactory.kt @@ -102,7 +102,7 @@ internal class TrackedEntityInstanceQueryCallFactory( return coroutineAPICallExecutor.wrap(storeError = false) { eventService.getEvents( fields = EventFields.teiQueryFields, - orgUnit = getOrgunits(orgunit, query.orgUnitMode), + orgUnit = getOrgunits(orgunit, query.orgUnitMode)?.firstOrNull(), orgUnitMode = query.orgUnitMode?.toString(), status = query.eventStatus?.toString(), program = query.program, @@ -127,16 +127,14 @@ internal class TrackedEntityInstanceQueryCallFactory( } private suspend fun getTrackedEntityQuery(query: TrackedEntityInstanceQueryOnline): List { - val uidsStr = query.uids?.joinToString(";") - return try { coroutineAPICallExecutor.wrap( storeError = false, errorCatcher = TrackedEntityInstanceQueryErrorCatcher(), ) { trackedEntityService.query( - trackedEntityInstance = uidsStr, - orgUnit = getOrgunits(query), + trackedEntityInstance = query.uids?.joinToString(";"), + orgUnit = getOrgunits(query)?.joinToString(";"), orgUnitMode = query.orgUnitMode?.toString(), program = query.program, programStage = query.programStage, diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt new file mode 100644 index 0000000000..a42e231490 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.tracker.exporter + +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode +import org.hisp.dhis.android.core.systeminfo.DHISVersion +import org.hisp.dhis.android.core.systeminfo.DHISVersionManager +import org.koin.core.annotation.Singleton + +@Singleton +internal class TrackerExporterParameterManager( + private val dhisVersionManager: DHISVersionManager, +) { + fun getTrackedEntityParameter(uids: Collection?): Map { + return if (uids.isNullOrEmpty()) { + emptyMap() + } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + mapOf(TrackerExporterService.TRACKED_ENTITIES to uids.joinToString(",")) + } else { + mapOf(TrackerExporterService.TRACKED_ENTITY to uids.joinToString(";")) + } + } + + fun getEventParameter(uids: Collection?): Map { + return if (uids.isNullOrEmpty()) { + emptyMap() + } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + mapOf(TrackerExporterService.EVENTS to uids.joinToString(",")) + } else { + mapOf(TrackerExporterService.EVENT to uids.joinToString(";")) + } + } + + fun getOrgunitModeParameter(mode: OrganisationUnitMode?): Map { + return if (mode == null) { + emptyMap() + } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + mapOf(TrackerExporterService.OU_MODE to mode.name) + } else { + mapOf(TrackerExporterService.OU_MODE_BELOW_41 to mode.name) + } + } + + fun getOrgunitsParameter(uids: Collection?): Map { + return if (uids.isNullOrEmpty()) { + emptyMap() + } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + mapOf(TrackerExporterService.ORG_UNITS to uids.joinToString(",")) + } else { + mapOf(TrackerExporterService.ORG_UNIT to uids.joinToString(";")) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt index dab7ae63d4..58599f6a92 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterService.kt @@ -37,23 +37,23 @@ import retrofit2.http.* @Suppress("LongParameterList") internal interface TrackerExporterService { - @GET("$TRACKED_ENTITY_INSTANCES/{$TRACKED_ENTITY_INSTACE}") + @GET("$TRACKED_ENTITIES_API/{$TRACKED_ENTITY}") suspend fun getSingleTrackedEntityInstance( - @Path(TRACKED_ENTITY_INSTACE) trackedEntityInstanceUid: String, + @Path(TRACKED_ENTITY) trackedEntityInstanceUid: String, @Query(FIELDS) @Which fields: Fields, - @Query(OU_MODE) orgUnitMode: String?, + @QueryMap orgUnitMode: Map = emptyMap(), @Query(PROGRAM) program: String?, @Query(PROGRAM_STATUS) programStatus: String?, @Query(ENROLLMENT_ENROLLED_AFTER) programStartDate: String?, @Query(INCLUDE_DELETED) includeDeleted: Boolean, ): NewTrackerImporterTrackedEntity - @GET(TRACKED_ENTITY_INSTANCES) + @GET(TRACKED_ENTITIES_API) suspend fun getTrackedEntityInstances( @Query(FIELDS) @Which fields: Fields, - @Query(TRACKED_ENTITY_INSTACE) trackedEntityInstances: String? = null, - @Query(OU) orgUnits: String? = null, - @Query(OU_MODE) orgUnitMode: String? = null, + @QueryMap trackedEntityInstances: Map = emptyMap(), + @QueryMap orgUnits: Map = emptyMap(), + @QueryMap orgUnitMode: Map = emptyMap(), @Query(PROGRAM) program: String? = null, @Query(PROGRAM_STAGE) programStage: String? = null, @Query(ENROLLMENT_ENROLLED_AFTER) programStartDate: String? = null, @@ -77,17 +77,17 @@ internal interface TrackerExporterService { @Query(INCLUDE_DELETED) includeDeleted: Boolean = false, ): TrackerPayload - @GET("$ENROLLMENTS/{$ENROLLMENT}") + @GET("$ENROLLMENTS_API/{$ENROLLMENT}") suspend fun getEnrollmentSingle( @Path(ENROLLMENT) enrollmentUid: String, @Query(FIELDS) @Which fields: Fields, ): NewTrackerImporterEnrollment - @GET(EVENTS) + @GET(EVENTS_API) suspend fun getEvents( @Query(FIELDS) @Which fields: Fields, - @Query(OU) orgUnit: String?, - @Query(OU_MODE) orgUnitMode: String?, + @Query(ORG_UNIT) orgUnit: String?, + @QueryMap orgUnitMode: Map = emptyMap(), @Query(STATUS) status: String? = null, @Query(PROGRAM) program: String?, @Query(PROGRAM_STAGE) programStage: String? = null, @@ -111,25 +111,29 @@ internal interface TrackerExporterService { @Query(UPDATED_AFTER) updatedAfter: String?, @Query(UPDATED_BEFORE) updatedBefore: String? = null, @Query(INCLUDE_DELETED) includeDeleted: Boolean, - @Query(EVENT) eventUid: String? = null, + @QueryMap eventUid: Map = emptyMap(), ): TrackerPayload - @GET(EVENTS) + @GET(EVENTS_API) suspend fun getEventSingle( @Query(FIELDS) @Which fields: Fields, - @Query(EVENT) eventUid: String, - @Query(OU_MODE) orgUnitMode: String, + @QueryMap eventUid: Map? = null, + @QueryMap orgUnitMode: Map? = null, ): TrackerPayload companion object { - const val TRACKED_ENTITY_INSTANCES = "tracker/trackedEntities" - const val ENROLLMENTS = "tracker/enrollments" - const val EVENTS = "tracker/events" - const val TRACKED_ENTITY_INSTACE = "trackedEntity" + const val TRACKED_ENTITIES_API = "tracker/trackedEntities" + const val ENROLLMENTS_API = "tracker/enrollments" + const val EVENTS_API = "tracker/events" + const val TRACKED_ENTITY = "trackedEntity" + const val TRACKED_ENTITIES = "trackedEntities" const val ENROLLMENT = "enrollment" const val EVENT = "event" - const val OU = "orgUnit" - const val OU_MODE = "ouMode" + const val EVENTS = "events" + const val ORG_UNIT = "orgUnit" + const val ORG_UNITS = "orgUnits" + const val OU_MODE = "orgUnitMode" + const val OU_MODE_BELOW_41 = "ouMode" const val FIELDS = "fields" const val PAGING = "paging" const val PAGE = "page" diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt index 19d164960e..adb4e869cf 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerQueryHelper.kt @@ -32,19 +32,19 @@ import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline internal object TrackerQueryHelper { - fun getOrgunits(query: TrackerAPIQuery): String? { + fun getOrgunits(query: TrackerAPIQuery): List? { return getOrgunits(query.orgUnit?.let { listOf(it) }, query.commonParams.ouMode) } - fun getOrgunits(query: TrackedEntityInstanceQueryOnline): String? { + fun getOrgunits(query: TrackedEntityInstanceQueryOnline): List? { return getOrgunits(query.orgUnits, query.orgUnitMode) } - fun getOrgunits(orgunit: String?, mode: OrganisationUnitMode?): String? { + fun getOrgunits(orgunit: String?, mode: OrganisationUnitMode?): List? { return getOrgunits(orgunit?.let { listOf(it) }, mode) } - private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): String? { + private fun getOrgunits(orgunits: List?, mode: OrganisationUnitMode?): List? { return if (orgunits.isNullOrEmpty()) { null } else if ( @@ -54,7 +54,7 @@ internal object TrackerQueryHelper { ) { null } else { - orgunits.joinToString(";") + orgunits } } } From b78f2a825d7836b49f5b009abdcbb5326f6ca93d Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 1 Mar 2024 12:35:44 +0100 Subject: [PATCH 154/222] [ANDROSDK-1823] Adapt ownerhisp and transfer parameters --- .../trackedentity/api/BreakTheGlassAPIShould.kt | 8 +++++++- .../event/internal/NewEventEndpointCallFactory.kt | 4 ++-- .../internal/NewTrackedEntityEndpointCallFactory.kt | 4 ++-- .../internal/TrackedEntityEndpointCallFactory.kt | 1 - .../trackedentity/ownership/OwnershipManagerImpl.kt | 8 +++++++- .../trackedentity/ownership/OwnershipService.kt | 6 ++++-- .../trackedentity/ownership/ProgramOwnerPostCall.kt | 4 +++- .../exporter/TrackerExporterParameterManager.kt | 13 +++++++++++-- .../ownership/OwnershipManagerShould.kt | 3 +++ 9 files changed, 39 insertions(+), 12 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/BreakTheGlassAPIShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/BreakTheGlassAPIShould.kt index d02d2b6be8..73d45169a8 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/BreakTheGlassAPIShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/api/BreakTheGlassAPIShould.kt @@ -176,7 +176,13 @@ class BreakTheGlassAPIShould : BaseRealIntegrationTest() { } val glassResponse: HttpMessageResponse = - executor.wrap { ownershipService.breakGlass(tei.uid(), program, "Sync") }.getOrThrow() + executor.wrap { + ownershipService.breakGlass( + mapOf(OwnershipService.TRACKED_ENTITY to tei.uid()), + program, + "Sync", + ) + }.getOrThrow() val response2 = postTrackedEntities(tei) assertThat(response2.response()!!.status()).isEqualTo(ImportStatus.SUCCESS) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt index 3780b27479..20093bef27 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/NewEventEndpointCallFactory.kt @@ -57,13 +57,13 @@ internal class NewEventEndpointCallFactory( pageSize = eventQuery.pageSize, updatedAfter = eventQuery.lastUpdatedStr, includeDeleted = true, - eventUid = parameterManager.getEventParameter(eventQuery.uids), + eventUid = parameterManager.getEventsParameter(eventQuery.uids), ).let { mapPayload(it) } } override suspend fun getRelationshipEntityCall(item: RelationshipItemRelative): Payload { return service.getEventSingle( - eventUid = parameterManager.getEventParameter(listOf(item.itemUid)), + eventUid = parameterManager.getEventsParameter(listOf(item.itemUid)), fields = NewEventFields.asRelationshipFields, orgUnitMode = parameterManager.getOrgunitModeParameter(OrganisationUnitMode.ACCESSIBLE), ).let { mapPayload(it) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt index 7ffd597867..c5da0add1e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/NewTrackedEntityEndpointCallFactory.kt @@ -65,7 +65,7 @@ internal class NewTrackedEntityEndpointCallFactory( override suspend fun getCollectionCall(query: TrackerAPIQuery): Payload { return trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.allFields, - trackedEntityInstances = parameterManager.getTrackedEntityParameter(query.uids), + trackedEntityInstances = parameterManager.getTrackedEntitiesParameter(query.uids), orgUnits = parameterManager.getOrgunitsParameter(getOrgunits(query)), orgUnitMode = parameterManager.getOrgunitModeParameter(query.commonParams.ouMode), program = query.commonParams.program, @@ -181,7 +181,7 @@ internal class NewTrackedEntityEndpointCallFactory( ) { val payload = trackedExporterService.getTrackedEntityInstances( fields = NewTrackedEntityInstanceFields.asRelationshipFields, - trackedEntityInstances = parameterManager.getTrackedEntityParameter(query.uids), + trackedEntityInstances = parameterManager.getTrackedEntitiesParameter(query.uids), orgUnits = parameterManager.getOrgunitsParameter(getOrgunits(query)), orgUnitMode = parameterManager.getOrgunitModeParameter(query.orgUnitMode), program = query.program, diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt index 09edfb94dd..93875c2da3 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityEndpointCallFactory.kt @@ -28,7 +28,6 @@ package org.hisp.dhis.android.core.trackedentity.internal import org.hisp.dhis.android.core.arch.api.payload.internal.Payload -import org.hisp.dhis.android.core.arch.helpers.CollectionsHelper import org.hisp.dhis.android.core.relationship.internal.RelationshipItemRelative import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnline diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerImpl.kt index 66c9d985ad..399f823958 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerImpl.kt @@ -40,6 +40,7 @@ import org.hisp.dhis.android.core.imports.internal.HttpMessageResponse import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.maintenance.D2ErrorComponent +import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterParameterManager import org.koin.core.annotation.Singleton import java.util.* @@ -50,6 +51,7 @@ internal class OwnershipManagerImpl( private val dataStatePropagator: DataStatePropagator, private val programTempOwnerStore: ProgramTempOwnerStore, private val programOwnerStore: ProgramOwnerStore, + private val parameterManager: TrackerExporterParameterManager, ) : OwnershipManager { override fun breakGlass(trackedEntityInstance: String, program: String, reason: String): Completable { @@ -132,7 +134,11 @@ internal class OwnershipManagerImpl( reason: String, ): Result { return coroutineAPICallExecutor.wrap(storeError = true) { - ownershipService.breakGlass(trackedEntityInstance, program, reason) + ownershipService.breakGlass( + parameterManager.getTrackedEntityForOwnershipParameter(trackedEntityInstance), + program, + reason, + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipService.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipService.kt index 7bd9e3f442..b1caa1ec6f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipService.kt @@ -31,19 +31,20 @@ import org.hisp.dhis.android.core.imports.internal.HttpMessageResponse import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Query +import retrofit2.http.QueryMap internal interface OwnershipService { @POST("$OWNERSHIP_URL/override") suspend fun breakGlass( - @Query(TRACKED_ENTITY_INSTACE) trackedEntityInstance: String, + @QueryMap trackedEntity: Map, @Query(PROGRAM) program: String, @Query(REASON) reason: String, ): HttpMessageResponse @PUT("$OWNERSHIP_URL/transfer") suspend fun transfer( - @Query(TRACKED_ENTITY_INSTACE) trackedEntityInstance: String, + @QueryMap trackedEntity: Map, @Query(PROGRAM) program: String, @Query(ORG_UNIT) ou: String, ): HttpMessageResponse @@ -52,6 +53,7 @@ internal interface OwnershipService { const val OWNERSHIP_URL = "tracker/ownership" const val TRACKED_ENTITY_INSTACE = "trackedEntityInstance" + const val TRACKED_ENTITY = "trackedEntity" const val PROGRAM = "program" const val REASON = "reason" const val ORG_UNIT = "ou" diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/ProgramOwnerPostCall.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/ProgramOwnerPostCall.kt index de7d6226a8..34d996103a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/ProgramOwnerPostCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/ownership/ProgramOwnerPostCall.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.trackedentity.ownership import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutor import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.internal.DataStatePropagator +import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterParameterManager import org.koin.core.annotation.Singleton @Singleton @@ -38,12 +39,13 @@ internal class ProgramOwnerPostCall( private val coroutineAPICallExecutor: CoroutineAPICallExecutor, private val programOwnerStore: ProgramOwnerStore, private val dataStatePropagator: DataStatePropagator, + private val parameterManager: TrackerExporterParameterManager, ) { suspend fun uploadProgramOwner(programOwner: ProgramOwner) { val response = coroutineAPICallExecutor.wrap(storeError = true) { ownershipService.transfer( - programOwner.trackedEntityInstance(), + parameterManager.getTrackedEntityForOwnershipParameter(programOwner.trackedEntityInstance()), programOwner.program(), programOwner.ownerOrgUnit(), ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt index a42e231490..18cbc34bd6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/exporter/TrackerExporterParameterManager.kt @@ -30,13 +30,14 @@ package org.hisp.dhis.android.core.tracker.exporter import org.hisp.dhis.android.core.organisationunit.OrganisationUnitMode import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.hisp.dhis.android.core.systeminfo.DHISVersionManager +import org.hisp.dhis.android.core.trackedentity.ownership.OwnershipService import org.koin.core.annotation.Singleton @Singleton internal class TrackerExporterParameterManager( private val dhisVersionManager: DHISVersionManager, ) { - fun getTrackedEntityParameter(uids: Collection?): Map { + fun getTrackedEntitiesParameter(uids: Collection?): Map { return if (uids.isNullOrEmpty()) { emptyMap() } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { @@ -46,7 +47,7 @@ internal class TrackerExporterParameterManager( } } - fun getEventParameter(uids: Collection?): Map { + fun getEventsParameter(uids: Collection?): Map { return if (uids.isNullOrEmpty()) { emptyMap() } else if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { @@ -75,4 +76,12 @@ internal class TrackerExporterParameterManager( mapOf(TrackerExporterService.ORG_UNIT to uids.joinToString(";")) } } + + fun getTrackedEntityForOwnershipParameter(uid: String): Map { + return if (dhisVersionManager.isGreaterOrEqualThan(DHISVersion.V2_41)) { + mapOf(OwnershipService.TRACKED_ENTITY to uid) + } else { + mapOf(OwnershipService.TRACKED_ENTITY_INSTACE to uid) + } + } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerShould.kt index ae2847a208..e84c6bde0a 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/ownership/OwnershipManagerShould.kt @@ -36,6 +36,7 @@ import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallEx import org.hisp.dhis.android.core.common.internal.DataStatePropagator import org.hisp.dhis.android.core.imports.internal.HttpMessageResponse import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.tracker.exporter.TrackerExporterParameterManager import org.junit.Assert.fail import org.junit.Before import org.junit.Test @@ -51,6 +52,7 @@ class OwnershipManagerShould { private val dataStatePropagator: DataStatePropagator = mock() private val programTempOwnerStore: ProgramTempOwnerStore = mock() private val programOwnerStore: ProgramOwnerStore = mock() + private val parameterManager: TrackerExporterParameterManager = mock() private val httpResponse: HttpMessageResponse = mock() @@ -68,6 +70,7 @@ class OwnershipManagerShould { dataStatePropagator, programTempOwnerStore, programOwnerStore, + parameterManager, ) } From b650d55108fee61fcdd3b84f4de084dc299547fa Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 28 Feb 2024 20:05:56 +0100 Subject: [PATCH 155/222] [ANDROSDK-1801] Add FunctionContains --- .../parser/internal/expression/ParserUtils.kt | 1 + .../expression/function/FunctionContains.kt | 53 +++++++++++++++++++ .../expression/ExpressionServiceShould.kt | 49 ++++++++++++----- gradle/libs.versions.toml | 2 +- 4 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt index dbe9b1c711..d8bb893cee 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt @@ -93,6 +93,7 @@ internal object ParserUtils { ExpressionParser.LEAST to FunctionLeast(), ExpressionParser.LOG to FunctionLog(), ExpressionParser.LOG10 to FunctionLog10(), + ExpressionParser.CONTAINS to FunctionContains(), // Common variables ExpressionParser.OUG_BRACE to ItemOrgUnitGroup(), diff --git a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt new file mode 100644 index 0000000000..26bfa93cbc --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.parser.internal.expression.function + +import org.hisp.dhis.android.core.parser.internal.expression.CommonExpressionVisitor +import org.hisp.dhis.android.core.parser.internal.expression.ExpressionItem +import org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext + +internal class FunctionContains : ExpressionItem { + + override fun evaluate(ctx: ExprContext, visitor: CommonExpressionVisitor): Any { + val values = ctx.expr().map(visitor::castStringVisit) + + val targetValue = values[0] + val containedValues = values.drop(1) + + return containedValues.all { targetValue.contains(it) } + } + + override fun getSql(ctx: ExprContext, visitor: CommonExpressionVisitor): Any { + val values = ctx.expr().map(visitor::castStringVisit) + + val targetValue = values[0] + val containedValues = values.drop(1) + + return "(${containedValues.joinToString(" AND ") { "$targetValue LIKE '%$it%'" }})" + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt b/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt index 2cdffdcc04..24f655e780 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt @@ -273,6 +273,25 @@ class ExpressionServiceShould { assertThat(service.getExpressionValue("is('three' in 'one', 'two')")).isEqualTo(false) } + @Test + fun evaluate_contains() { + assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'MOLD_ALLERGY')", true) + assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'LATEX_ALLERGY', 'MOLD_ALLERGY')", true) + assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'ALLERGY')", true) + assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'RGY,LAT')", true) + assertEqual("contains('abcdef', 'abcdef')", true) + assertEqual("contains('abcdef', 'bcd')", true) + assertEqual("contains('abcdef', 'xyz')", false) + + assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'MOLD_ALLERGY')", true) + assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'LATEX_ALLERGY', 'MOLD_ALLERGY')", true) + assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'ALLERGY')", false) + assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'RGY,LAT')", false) + assertEqual("containsItems('abcdef', 'abcdef')", true) + assertEqual("containsItems('abcdef', 'bcd')", false) + assertEqual("containsItems('abcdef', 'xyz')", false) + } + @Test fun evaluate_divide_by_zero() { assertThat(service.getExpressionValue("4 / 0")).isEqualTo(null) @@ -315,9 +334,9 @@ class ExpressionServiceShould { @Test fun get_dataelement_ids_in_nested_expressions() { val expression = "if(" + - "${de(dataElementId1)} > 0, " + - "greatest(${de(dataElementId1)}, ${de(dataElementId2)})," + - "${deOperand(dataElementId1, categoryOptionComboId1)})" + "${de(dataElementId1)} > 0, " + + "greatest(${de(dataElementId1)}, ${de(dataElementId2)})," + + "${deOperand(dataElementId1, categoryOptionComboId1)})" val dataElementOperands = service.getDataElementOperands(expression) assertThat(dataElementOperands.size).isEqualTo(3) @@ -339,9 +358,9 @@ class ExpressionServiceShould { whenever(constant.displayName()).thenReturn("Constant") val expression = deOperand(dataElementId1, categoryOptionComboId1) + " + " + - de(dataElementId2) + " * " + - oug(orgunitGroupId) + " + " + - constant(constantId) + de(dataElementId2) + " * " + + oug(orgunitGroupId) + " + " + + constant(constantId) val description = service.getExpressionDescription(expression, constantMap) assertThat(description).isEqualTo("Data Element 1 (COC 1) + Data Element 2 * Org Unit Group + Constant") @@ -360,13 +379,13 @@ class ExpressionServiceShould { @Test fun regenerate_expression() { val expression = deOperand(dataElementId1, categoryOptionComboId1) + " + " + - de(dataElementId2) + " / " + - constant(constantId) + " * " + - oug(orgunitGroupId) + " - " + - "5.0 + " + - "'expr' + " + - "true + " + - days + de(dataElementId2) + " / " + + constant(constantId) + " * " + + oug(orgunitGroupId) + " - " + + "5.0 + " + + "'expr' + " + + "true + " + + days val valueMap: Map = mapOf( DataElementOperandObject(dataElementId1, categoryOptionComboId1) to 5.0, DataElementObject(dataElementId2) to 3.0, @@ -396,6 +415,10 @@ class ExpressionServiceShould { assertThat(regeneratedExpression).isEqualTo("5.0 + " + de(dataElementId2)) } + private fun assertEqual(expression: String, result: Any) { + assertThat(service.getExpressionValue(expression)).isEqualTo(result) + } + private fun constant(uid: String): String { return "C{$uid}" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f5e1080c1..caf97e71b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ rxAndroid = "2.1.1" sqlCipher = "4.5.5" sqlLite = "2.2.0" smsCompression = "0.2.0" -expressionParser = "1.0.33" +expressionParser = "1.0.36" # Kotlin kotlinxDatetime = "0.4.1" From cd6dea9a518a062d82b21b4cc8129855dff2b589 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 1 Mar 2024 16:33:04 +0100 Subject: [PATCH 156/222] [ANDROSDK-1801] Add FunctionContainsItems --- .../BaseEvaluatorIntegrationShould.kt | 2 + .../evaluator/BaseEvaluatorSamples.kt | 6 ++ ...rackerLineListRepositoryEvaluatorShould.kt | 36 ++++++++++++ .../parser/internal/expression/ParserUtils.kt | 1 + .../expression/function/FunctionContains.kt | 2 +- .../function/FunctionContainsItems.kt | 56 +++++++++++++++++++ .../expression/ExpressionServiceShould.kt | 42 +++++++------- 7 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContainsItems.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorIntegrationShould.kt index 5652ca1ce8..3f66b6de7b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorIntegrationShould.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute2 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attributeAttributeComboLink import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attributeAttributeOptionLink import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attributeCombo @@ -248,6 +249,7 @@ internal open class BaseEvaluatorIntegrationShould : BaseMockIntegrationTestEmpt trackedEntityTypeStore.insert(trackedEntityType) trackedEntityAttributeStore.insert(attribute1) + trackedEntityAttributeStore.insert(attribute2) programStore.insert(program) programStageStore.insert(programStage1) programStageStore.insert(programStage2) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt index c2e264a8d6..808bc475e2 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/aggregated/internal/evaluator/BaseEvaluatorSamples.kt @@ -220,6 +220,12 @@ object BaseEvaluatorSamples { .valueType(ValueType.INTEGER) .build() + val attribute2 = TrackedEntityAttribute.builder() + .uid(generator.generate()) + .displayName("Attribute 2") + .valueType(ValueType.TEXT) + .build() + val period2019SunW25: Period = Period.builder() .periodId("2019SunW25") .periodType(PeriodType.WeeklySunday) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 164a00ee6e..1e72c1c17b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorIntegrationShould import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.attribute2 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.dataElement1 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.generator import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild1 @@ -215,6 +216,41 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati assertThat(rows[1][1].value).isEqualTo("133") } + @Test + fun evaluate_contains_functions() { + helper.createTrackedEntity(trackedEntity1.uid(), orgunitChild1.uid(), trackedEntityType.uid()) + val enrollment1 = generator.generate() + createDefaultEnrollment(trackedEntity1.uid(), enrollment1, enrollmentDate = period202001.startDate()) + helper.insertTrackedEntityAttributeValue(trackedEntity1.uid(), attribute2.uid(), "ALLERGY,LATEX") + + fun evaluateExpression(expression: String, expected: String) { + val programIndicator = generator.generate() + helper.setProgramIndicatorExpression( + programIndicator, + program.uid(), + expression = expression, + analyticsType = AnalyticsType.ENROLLMENT, + ) + + val result = d2.analyticsModule().trackerLineList() + .withEnrollmentOutput(program.uid()) + .withColumn(TrackerLineListItem.ProgramIndicator(programIndicator)) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(1) + assertThat(rows.first().first().value).isEqualTo(expected) + } + + evaluateExpression("if(contains(A{${attribute2.uid()}}, 'LATEX', 'ALLERGY'), '1', '0')", "1") + evaluateExpression("if(contains(A{${attribute2.uid()}}, 'GY,LA'), '1', '0')", "1") + evaluateExpression("if(contains(A{${attribute2.uid()}}, 'xy', 'ALLERGY'), '1', '0')", "0") + + evaluateExpression("if(containsItems(A{${attribute2.uid()}}, 'LATEX', 'ALLERGY'), '1', '0')", "1") + evaluateExpression("if(containsItems(A{${attribute2.uid()}}, 'GY,LA'), '1', '0')", "0") + evaluateExpression("if(containsItems(A{${attribute2.uid()}}, 'xy', 'ALLERGY'), '1', '0')", "0") + } + @Test fun evaluate_single_events() { val event1 = generator.generate() diff --git a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt index d8bb893cee..dc4e29a58f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/ParserUtils.kt @@ -94,6 +94,7 @@ internal object ParserUtils { ExpressionParser.LOG to FunctionLog(), ExpressionParser.LOG10 to FunctionLog10(), ExpressionParser.CONTAINS to FunctionContains(), + ExpressionParser.CONTAINS_ITEMS to FunctionContainsItems(), // Common variables ExpressionParser.OUG_BRACE to ItemOrgUnitGroup(), diff --git a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt index 26bfa93cbc..293f00f486 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContains.kt @@ -46,7 +46,7 @@ internal class FunctionContains : ExpressionItem { val values = ctx.expr().map(visitor::castStringVisit) val targetValue = values[0] - val containedValues = values.drop(1) + val containedValues = values.drop(1).map { it.trimStart('\'').trimEnd('\'') } return "(${containedValues.joinToString(" AND ") { "$targetValue LIKE '%$it%'" }})" } diff --git a/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContainsItems.kt b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContainsItems.kt new file mode 100644 index 0000000000..2ecc24c7b7 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/parser/internal/expression/function/FunctionContainsItems.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.parser.internal.expression.function + +import org.hisp.dhis.android.core.parser.internal.expression.CommonExpressionVisitor +import org.hisp.dhis.android.core.parser.internal.expression.ExpressionItem +import org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext + +internal class FunctionContainsItems : ExpressionItem { + + override fun evaluate(ctx: ExprContext, visitor: CommonExpressionVisitor): Any { + val values = ctx.expr().map(visitor::castStringVisit) + + val targetValues = values[0].split(",") + val containedValues = values.drop(1) + + return containedValues.all { targetValues.contains(it) } + } + + override fun getSql(ctx: ExprContext, visitor: CommonExpressionVisitor): Any { + val values = ctx.expr().map(visitor::castStringVisit) + + val targetValue = values[0] + val containedValues = values.drop(1).map { it.trimStart('\'').trimEnd('\'') } + + val sql = "(${containedValues.joinToString(" AND ") { + "($targetValue LIKE '%,$it' OR $targetValue LIKE '$it,%' OR $targetValue LIKE '%,$it,%')" + }})" + return sql + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt b/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt index 24f655e780..cfb9e51963 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/parser/internal/expression/ExpressionServiceShould.kt @@ -275,18 +275,18 @@ class ExpressionServiceShould { @Test fun evaluate_contains() { - assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'MOLD_ALLERGY')", true) - assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'LATEX_ALLERGY', 'MOLD_ALLERGY')", true) - assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'ALLERGY')", true) - assertEqual("contains('MOLD_ALLERGY,LATEX_ALLERGY', 'RGY,LAT')", true) + assertEqual("contains('ALLERGY,LATEX', 'ALLERGY')", true) + assertEqual("contains('ALLERGY,LATEX', 'LATEX', 'ALLERGY')", true) + assertEqual("contains('ALLERGY,LATEX', 'ALLE')", true) + assertEqual("contains('ALLERGY,LATEX', 'RGY,LAT')", true) assertEqual("contains('abcdef', 'abcdef')", true) assertEqual("contains('abcdef', 'bcd')", true) assertEqual("contains('abcdef', 'xyz')", false) - assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'MOLD_ALLERGY')", true) - assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'LATEX_ALLERGY', 'MOLD_ALLERGY')", true) - assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'ALLERGY')", false) - assertEqual("containsItems('MOLD_ALLERGY,LATEX_ALLERGY', 'RGY,LAT')", false) + assertEqual("containsItems('ALLERGY,LATEX', 'ALLERGY')", true) + assertEqual("containsItems('ALLERGY,LATEX', 'LATEX', 'ALLERGY')", true) + assertEqual("containsItems('ALLERGY,LATEX', 'ALLE')", false) + assertEqual("containsItems('ALLERGY,LATEX', 'RGY,LAT')", false) assertEqual("containsItems('abcdef', 'abcdef')", true) assertEqual("containsItems('abcdef', 'bcd')", false) assertEqual("containsItems('abcdef', 'xyz')", false) @@ -334,9 +334,9 @@ class ExpressionServiceShould { @Test fun get_dataelement_ids_in_nested_expressions() { val expression = "if(" + - "${de(dataElementId1)} > 0, " + - "greatest(${de(dataElementId1)}, ${de(dataElementId2)})," + - "${deOperand(dataElementId1, categoryOptionComboId1)})" + "${de(dataElementId1)} > 0, " + + "greatest(${de(dataElementId1)}, ${de(dataElementId2)})," + + "${deOperand(dataElementId1, categoryOptionComboId1)})" val dataElementOperands = service.getDataElementOperands(expression) assertThat(dataElementOperands.size).isEqualTo(3) @@ -358,9 +358,9 @@ class ExpressionServiceShould { whenever(constant.displayName()).thenReturn("Constant") val expression = deOperand(dataElementId1, categoryOptionComboId1) + " + " + - de(dataElementId2) + " * " + - oug(orgunitGroupId) + " + " + - constant(constantId) + de(dataElementId2) + " * " + + oug(orgunitGroupId) + " + " + + constant(constantId) val description = service.getExpressionDescription(expression, constantMap) assertThat(description).isEqualTo("Data Element 1 (COC 1) + Data Element 2 * Org Unit Group + Constant") @@ -379,13 +379,13 @@ class ExpressionServiceShould { @Test fun regenerate_expression() { val expression = deOperand(dataElementId1, categoryOptionComboId1) + " + " + - de(dataElementId2) + " / " + - constant(constantId) + " * " + - oug(orgunitGroupId) + " - " + - "5.0 + " + - "'expr' + " + - "true + " + - days + de(dataElementId2) + " / " + + constant(constantId) + " * " + + oug(orgunitGroupId) + " - " + + "5.0 + " + + "'expr' + " + + "true + " + + days val valueMap: Map = mapOf( DataElementOperandObject(dataElementId1, categoryOptionComboId1) to 5.0, DataElementObject(dataElementId2) to 3.0, From cf5f13648e6530cf825156c987a234e3ee4fbe18 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 1 Mar 2024 16:38:00 +0100 Subject: [PATCH 157/222] [ANDROSDK-1801] Update gradle plugin to 8.3.0 --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index caf97e71b3..ecfc9a1f91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -gradle = "8.2.2" +gradle = "8.3.0" kotlin = "1.9.21" ktlint = "11.5.1" jacoco = "0.8.10" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2a67a7b3d..7166abc811 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Aug 31 00:09:15 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From f49688332eba3802daa0d82b648fb7723cc3e086 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:56:02 +0100 Subject: [PATCH 158/222] [ANDROSDK-1816] Add service to download versions --- .../dhis/android/core/settings/internal/SettingAppService.kt | 4 ++++ .../dhis/android/core/settings/internal/SettingService.kt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt index 31d96802de..61377a699e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt @@ -79,6 +79,10 @@ internal class SettingAppService( return settingService.latestAppVersion("$APK_DISTRIBUTION_NAMESPACE/latestVersion") } + suspend fun versions(): List { + return settingService.versions("$APK_DISTRIBUTION_NAMESPACE/versions") + } + private fun getNamespace(version: SettingsAppDataStoreVersion): String { return when (version) { SettingsAppDataStoreVersion.V1_1 -> ANDROID_APP_NAMESPACE_V1 diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt index 9c53bdf686..9a523ec623 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt @@ -69,4 +69,7 @@ internal interface SettingService { @GET suspend fun latestAppVersion(@Url url: String): LatestAppVersion + + @GET + suspend fun versions(@Url url: String): List } From 75c95fa8afc8fe5e7e5968f7c0fdd24a376a081f Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:57:06 +0100 Subject: [PATCH 159/222] [ANDROSDK-1816] Add user groups to LatestAppVersion --- .../hisp/dhis/android/core/settings/LatestAppVersion.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java index be7a2b8b8c..55e5062fa1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java @@ -39,6 +39,8 @@ import org.hisp.dhis.android.core.common.CoreObject; +import java.util.List; + @AutoValue @JsonDeserialize(builder = $$AutoValue_LatestAppVersion.Builder.class) public abstract class LatestAppVersion implements CoreObject { @@ -51,6 +53,10 @@ public abstract class LatestAppVersion implements CoreObject { @Nullable public abstract String downloadURL(); + @JsonProperty() + @Nullable + public abstract List userGroups(); + public static LatestAppVersion create(Cursor cursor) { return $AutoValue_LatestAppVersion.createFromCursor(cursor); } @@ -70,6 +76,8 @@ public abstract static class Builder { public abstract Builder downloadURL(String downloadURL); + public abstract Builder userGroups(List userGroups); + public abstract LatestAppVersion build(); } } From dba997d6953aa10c93d42ab2d34121eb586daa09 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 26 Feb 2024 11:57:30 +0100 Subject: [PATCH 160/222] [ANDROSDK-1816] Temp: add versions call --- .../settings/internal/LatestAppVersionCall.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 3a129961ed..82f5f69385 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -30,19 +30,37 @@ package org.hisp.dhis.android.core.settings.internal import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutor import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.common.AssignedUserMode import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.LatestAppVersion +import org.hisp.dhis.android.core.user.UserModule import org.koin.core.annotation.Singleton @Singleton internal class LatestAppVersionCall( private val latestAppVersionHandler: LatestAppVersionHandler, private val settingAppService: SettingAppService, + private val userModule: UserModule, coroutineAPICallExecutor: CoroutineAPICallExecutor, ) : BaseSettingCall(coroutineAPICallExecutor) { override suspend fun tryFetch(storeError: Boolean): Result { - return coroutineAPICallExecutor.wrap(storeError = storeError) { settingAppService.latestAppVersion() } + + return coroutineAPICallExecutor.wrap(storeError = storeError) { + val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } + + val filteredVersions = settingAppService.versions().filter { version -> + version.userGroups()?.any { userGroupUid -> + userGroupUids.contains(userGroupUid) + } ?: false + } + + val highestVersion = filteredVersions.maxByOrNull { version -> + // TODO get hightest filtered version + } + + highestVersion ?: settingAppService.latestAppVersion() + } } override fun process(item: LatestAppVersion?) { From a6f3861165f193c1cda262de15fa8092c52af45d Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 4 Mar 2024 10:27:49 +0100 Subject: [PATCH 161/222] [ANDROSDK-1812] Refactor DatabaseExport to Kotlin --- .../core/arch/d2/internal/JavaDIClasses.kt | 1 - .../db/access/internal/DatabaseExport.java | 114 ----------------- .../arch/db/access/internal/DatabaseExport.kt | 115 ++++++++++++++++++ 3 files changed, 115 insertions(+), 115 deletions(-) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.java create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt index 951ca141cb..18ad25624b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt @@ -53,7 +53,6 @@ internal val javaDIClasses = module { single { DatabaseEncryptionPasswordGenerator() } single { TrackedEntityInstanceService(get(), get(), get(), get()) } single { DatabaseAdapterFactory(get(), get()) } - single { DatabaseExport(get(), get(), get()) } single { D2CallExecutor(get(), get()) } single { DataSetsStore(get(), get(), get(), get()) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.java deleted file mode 100644 index 127b3d8498..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.arch.db.access.internal; - -import android.content.Context; -import android.util.Log; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; -import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; - -import org.hisp.dhis.android.core.common.internal.NativeLibraryLoader; -import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount; -import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper; -import org.hisp.dhis.android.core.configuration.internal.DatabaseEncryptionPasswordManager; - -import java.io.File; - -import io.reactivex.functions.Action; - -@SuppressWarnings("PMD.ExcessiveImports") -public class DatabaseExport { - - private final Context context; - private final DatabaseEncryptionPasswordManager passwordManager; - private final DatabaseConfigurationHelper configurationHelper; - - public DatabaseExport(Context context, DatabaseEncryptionPasswordManager passwordManager, - DatabaseConfigurationHelper configurationHelper) { - this.context = context; - this.passwordManager = passwordManager; - this.configurationHelper = configurationHelper; - } - - public void encrypt(String serverUrl, DatabaseAccount oldConfiguration) { - DatabaseAccount newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration); - export(oldConfiguration, newConfiguration, null, - passwordManager.getPassword(newConfiguration.databaseName()), "Encrypt", null, - EncryptedDatabaseOpenHelper.hook); - } - - public void decrypt(String serverUrl, DatabaseAccount oldConfiguration) { - DatabaseAccount newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration); - export(oldConfiguration, newConfiguration, passwordManager.getPassword(oldConfiguration.databaseName()), - "", "Decrypt", EncryptedDatabaseOpenHelper.hook, null); - } - - private void export(DatabaseAccount oldConfiguration, - DatabaseAccount newConfiguration, String oldPassword, String newPassword, String tag, - SQLiteDatabaseHook oldHook, SQLiteDatabaseHook newHook) { - wrapAction(() -> { - File oldDatabaseFile = context.getDatabasePath(oldConfiguration.databaseName()); - File newDatabaseFile = context.getDatabasePath(newConfiguration.databaseName()); - - NativeLibraryLoader.INSTANCE.loadSQLCipher(); - SQLiteDatabase oldDatabase = SQLiteDatabase.openOrCreateDatabase(oldDatabaseFile, oldPassword, null, - null, oldHook); - oldDatabase.rawExecSQL(String.format( - "ATTACH DATABASE '%s' as alias KEY '%s';", newDatabaseFile.getAbsolutePath(), newPassword)); - if (newHook != null) { - oldDatabase.rawExecSQL("PRAGMA alias.cipher_page_size = 16384;"); - oldDatabase.rawExecSQL("PRAGMA alias.cipher_memory_security = OFF;"); - } - oldDatabase.rawExecSQL("SELECT sqlcipher_export('alias');"); - oldDatabase.rawExecSQL("DETACH DATABASE alias;"); - - int version = oldDatabase.getVersion(); - SQLiteDatabase newDatabase = SQLiteDatabase.openOrCreateDatabase(newDatabaseFile, newPassword, null, - null, newHook); - newDatabase.setVersion(version); - - newDatabase.close(); - oldDatabase.close(); - }, tag); - } - - @SuppressWarnings({"PMD.PrematureDeclaration", "PMD.PreserveStackTrace"}) - private void wrapAction(Action action, String tag) { - long startMillis = System.currentTimeMillis(); - try { - action.run(); - } catch (Exception e) { - throw new RuntimeException("Exception thrown during database export action: " + tag); - } - long endMillis = System.currentTimeMillis(); - - Log.e("DatabaseExport", tag + ": " + (endMillis - startMillis) + "ms"); - } -} \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt new file mode 100644 index 0000000000..eccbad5545 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.db.access.internal + +import android.content.Context +import android.util.Log +import io.reactivex.functions.Action +import net.zetetic.database.sqlcipher.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabaseHook +import org.hisp.dhis.android.core.common.internal.NativeLibraryLoader.loadSQLCipher +import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount +import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper +import org.hisp.dhis.android.core.configuration.internal.DatabaseEncryptionPasswordManager +import org.koin.core.annotation.Singleton + +@Singleton +internal class DatabaseExport( + private val context: Context, + private val passwordManager: DatabaseEncryptionPasswordManager, + private val configurationHelper: DatabaseConfigurationHelper +) { + fun encrypt(serverUrl: String, oldConfiguration: DatabaseAccount) { + val newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration) + export( + oldConfiguration, newConfiguration, null, + passwordManager.getPassword(newConfiguration.databaseName()), "Encrypt", null, + EncryptedDatabaseOpenHelper.hook + ) + } + + fun decrypt(serverUrl: String, oldConfiguration: DatabaseAccount) { + val newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration) + export( + oldConfiguration, newConfiguration, passwordManager.getPassword(oldConfiguration.databaseName()), + "", "Decrypt", EncryptedDatabaseOpenHelper.hook, null + ) + } + + private fun export( + oldConfiguration: DatabaseAccount, + newConfiguration: DatabaseAccount, + oldPassword: String?, + newPassword: String, + tag: String, + oldHook: SQLiteDatabaseHook?, + newHook: SQLiteDatabaseHook? + ) { + wrapAction({ + val oldDatabaseFile = context.getDatabasePath(oldConfiguration.databaseName()) + val newDatabaseFile = context.getDatabasePath(newConfiguration.databaseName()) + + loadSQLCipher() + + val oldDatabase = SQLiteDatabase.openOrCreateDatabase(oldDatabaseFile, oldPassword, null, null, oldHook) + oldDatabase.rawExecSQL( + String.format( + "ATTACH DATABASE '%s' as alias KEY '%s';", newDatabaseFile.absolutePath, newPassword + ) + ) + + if (newHook != null) { + oldDatabase.rawExecSQL("PRAGMA alias.cipher_page_size = 16384;") + oldDatabase.rawExecSQL("PRAGMA alias.cipher_memory_security = OFF;") + } + oldDatabase.rawExecSQL("SELECT sqlcipher_export('alias');") + oldDatabase.rawExecSQL("DETACH DATABASE alias;") + + val version = oldDatabase.version + val newDatabase = SQLiteDatabase.openOrCreateDatabase( + newDatabaseFile, newPassword, null, + null, newHook + ) + newDatabase.version = version + + newDatabase.close() + oldDatabase.close() + }, tag) + } + + private fun wrapAction(action: Action, tag: String) { + val startMillis = System.currentTimeMillis() + try { + action.run() + } catch (e: Exception) { + throw RuntimeException("Exception thrown during database export action: $tag") + } + val endMillis = System.currentTimeMillis() + Log.e("DatabaseExport", tag + ": " + (endMillis - startMillis) + "ms") + } +} From ca53f848cf241031b2fc6ff3a39173870731ce08 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:48:16 +0100 Subject: [PATCH 162/222] [ANDROSDK-1816] Add defaultVersion to latest app version --- .../dhis/android/core/settings/LatestAppVersion.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java index 55e5062fa1..3b48fca89d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java @@ -35,8 +35,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreStringListColumnAdapter; import org.hisp.dhis.android.core.common.CoreObject; import java.util.List; @@ -55,6 +57,11 @@ public abstract class LatestAppVersion implements CoreObject { @JsonProperty() @Nullable + public abstract Boolean defaultVersion(); + + @JsonProperty() + @Nullable + @ColumnAdapter(IgnoreStringListColumnAdapter.class) public abstract List userGroups(); public static LatestAppVersion create(Cursor cursor) { @@ -76,6 +83,8 @@ public abstract static class Builder { public abstract Builder downloadURL(String downloadURL); + public abstract Builder defaultVersion(Boolean defaultVersion); + public abstract Builder userGroups(List userGroups); public abstract LatestAppVersion build(); From 3a13f2bebc61d4e8f45e8718e2d598da207a3db7 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:48:46 +0100 Subject: [PATCH 163/222] [ANDROSDK-1816] Add LatestAppVersionComparator --- .../internal/LatestAppVersionComparator.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt new file mode 100644 index 0000000000..63112048b9 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings.internal + +import org.hisp.dhis.android.core.settings.LatestAppVersion +import org.koin.core.annotation.Singleton + +@Singleton +internal class LatestAppVersionComparator { + val comparator: Comparator = Comparator { v1, v2 -> + val partsV1 = v1.version()?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() + val partsV2 = v2.version()?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() + var result = 0 + val maxLength = maxOf(partsV1.size, partsV2.size) + for (i in 0 until maxLength) { + val partV1 = partsV1.getOrElse(i) { 0 } + val partV2 = partsV2.getOrElse(i) { 0 } + result = partV1.compareTo(partV2) + if (result != 0) break + } + result + } +} From 8dba5f46bf9b7febd0d8c5ffeac6fd9cdd78b8bb Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:49:03 +0100 Subject: [PATCH 164/222] [ANDROSDK-1816] Add IgnoreStringListColumnAdapter --- .../IgnoreStringListColumnAdapter.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java new file mode 100644 index 0000000000..b05d377115 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal; + +import java.util.List; + +public final class IgnoreStringListColumnAdapter extends IgnoreColumnAdapter> { +} From b3a1c8e224eb42236817ba6fdbd26613cbf03d05 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:49:32 +0100 Subject: [PATCH 165/222] [ANDROSDK-1816] Use comparator in latest app version call --- .../core/settings/internal/LatestAppVersionCall.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 82f5f69385..5f88630eb4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -30,7 +30,6 @@ package org.hisp.dhis.android.core.settings.internal import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutor import org.hisp.dhis.android.core.arch.helpers.Result -import org.hisp.dhis.android.core.common.AssignedUserMode import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.LatestAppVersion import org.hisp.dhis.android.core.user.UserModule @@ -41,6 +40,7 @@ internal class LatestAppVersionCall( private val latestAppVersionHandler: LatestAppVersionHandler, private val settingAppService: SettingAppService, private val userModule: UserModule, + private val versionComparator: LatestAppVersionComparator, coroutineAPICallExecutor: CoroutineAPICallExecutor, ) : BaseSettingCall(coroutineAPICallExecutor) { @@ -49,17 +49,17 @@ internal class LatestAppVersionCall( return coroutineAPICallExecutor.wrap(storeError = storeError) { val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } - val filteredVersions = settingAppService.versions().filter { version -> + val versions = settingAppService.versions() + + val filteredVersions = versions.filter { version -> version.userGroups()?.any { userGroupUid -> userGroupUids.contains(userGroupUid) } ?: false } - val highestVersion = filteredVersions.maxByOrNull { version -> - // TODO get hightest filtered version - } + val highestVersion = filteredVersions.maxWithOrNull(versionComparator.comparator) - highestVersion ?: settingAppService.latestAppVersion() + highestVersion ?: versions.find { it.defaultVersion() == true } ?: settingAppService.latestAppVersion() } } From 789943eb15b3d2fe61731b0e08bcd042d7335946 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:50:08 +0100 Subject: [PATCH 166/222] [ANDROSDK-1816] Add versions jsons --- .../core/mockwebserver/Dhis2MockServer.java | 7 ++- .../resources/settings/version.json | 6 +++ .../resources/settings/versions.json | 45 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 core/src/sharedTest/resources/settings/version.json create mode 100644 core/src/sharedTest/resources/settings/versions.json diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index f540c92aa6..7bedebd33b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -65,6 +65,7 @@ public class Dhis2MockServer { private static final String APPEARANCE_SETTINGS_JSON = "settings/appearance_settings_v2.json"; private static final String ANALYTICS_SETTINGS_JSON = "settings/analytics_settings_v3.json"; private static final String USER_SETTINGS_JSON = "settings/user_settings.json"; + private static final String VERSIONS_JSON = "settings/versions.json"; private static final String LATEST_APP_VERSION_JSON = "settings/latest_app_version.json"; private static final String PROGRAMS_JSON = "program/programs.json"; private static final String PROGRAMS_INDICATORS_JSON = "program/program_indicators.json"; @@ -222,7 +223,9 @@ public MockResponse dispatch(RecordedRequest request) { return createMockResponse(ANALYTICS_SETTINGS_JSON); } else if (path.startsWith("/api/userSettings?")) { return createMockResponse(USER_SETTINGS_JSON); - } else if (path.startsWith("/api/dataStore/APK_DISTRIBUTION/latestVersion")) { + } else if (path.startsWith("/api/dataStore/APK_DISTRIBUTION/versions")) { + return createMockResponse(VERSIONS_JSON); + } else if (path.startsWith("/api/dataStore/APK_DISTRIBUTION/latestVersion")) { return createMockResponse(LATEST_APP_VERSION_JSON); } else if (path.startsWith("/api/programs?")) { return createMockResponse(PROGRAMS_JSON); @@ -349,7 +352,7 @@ public void enqueueMetadataResponses() { enqueueMockResponse(ANALYTICS_SETTINGS_JSON); enqueueMockResponse(USER_SETTINGS_JSON); enqueueMockResponse(SYSTEM_SETTINGS_JSON); - enqueueMockResponse(LATEST_APP_VERSION_JSON); + enqueueMockResponse(VERSIONS_JSON); enqueueMockResponse(STOCK_USE_CASES_JSON); enqueueMockResponse(CONSTANTS_JSON); enqueueMockResponse(USER_JSON); diff --git a/core/src/sharedTest/resources/settings/version.json b/core/src/sharedTest/resources/settings/version.json new file mode 100644 index 0000000000..d39221b960 --- /dev/null +++ b/core/src/sharedTest/resources/settings/version.json @@ -0,0 +1,6 @@ +{ + "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", + "version": "40.1", + "defaultVersion": true, + "userGroups": ["Kk12LkEWtXp"] +} \ No newline at end of file diff --git a/core/src/sharedTest/resources/settings/versions.json b/core/src/sharedTest/resources/settings/versions.json new file mode 100644 index 0000000000..a08cd13005 --- /dev/null +++ b/core/src/sharedTest/resources/settings/versions.json @@ -0,0 +1,45 @@ +{ + "versions": [ + { + "androidOSVersion": { + "min": "5", + "recommended": "7" + }, + "dhis2Version": { + "min": "2.29", + "recommended": "2.40" + }, + "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.3/dhis2-40.3.apk", + "version": "40.3", + "defaultVersion": true + }, + { + "androidOSVersion": { + "min": "5", + "recommended": "7" + }, + "dhis2Version": { + "min": "2.29", + "recommended": "2.40" + }, + "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.2/dhis2-40.2.apk", + "version": "40.2", + "defaultVersion": false, + "userGroups": ["Kk12LkEWtXp"] + }, + { + "androidOSVersion": { + "min": "5", + "recommended": "7" + }, + "dhis2Version": { + "min": "2.29", + "recommended": "2.40" + }, + "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", + "version": "40.1", + "defaultVersion": false, + "userGroups": ["Kk12LkEWtXp"] + } + ] +} \ No newline at end of file From 5c8d8b8e957ed36d4b289443ed56ee04665055ab Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:50:28 +0100 Subject: [PATCH 167/222] [ANDROSDK-1816] Rename .java to .kt --- ...a => LatestAppVersionObjectRepositoryMockIntegrationShould.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/{LatestAppVersionObjectRepositoryMockIntegrationShould.java => LatestAppVersionObjectRepositoryMockIntegrationShould.kt} (100%) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt similarity index 100% rename from core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.java rename to core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt From 0e430ca6dce0b0567ad04699ad414cda8cb92168 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:50:29 +0100 Subject: [PATCH 168/222] [ANDROSDK-1816] Update integration tests --- ...onObjectRepositoryMockIntegrationShould.kt | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt index 8a5959b081..51d8d1ca11 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt @@ -25,25 +25,22 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.testapp.settings -package org.hisp.dhis.android.testapp.settings; - -import static com.google.common.truth.Truth.assertThat; - -import org.hisp.dhis.android.core.settings.LatestAppVersion; -import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher; -import org.hisp.dhis.android.core.utils.runner.D2JunitRunner; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(D2JunitRunner.class) -public class LatestAppVersionObjectRepositoryMockIntegrationShould extends BaseMockIntegrationTestFullDispatcher { +import com.google.common.truth.Truth +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestFullDispatcher +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.Test +import org.junit.runner.RunWith +@RunWith(D2JunitRunner::class) +class LatestAppVersionObjectRepositoryMockIntegrationShould : BaseMockIntegrationTestFullDispatcher() { @Test - public void find_user_settings() { - LatestAppVersion latestAppVersion = d2.settingModule().latestAppVersion().blockingGet(); - assertThat(latestAppVersion.downloadURL()).isEqualTo( - "https://github.com/dhis2/dhis2-android-capture-app/releases/download/2.7.1.1/dhis2-v2.7.1.1.apk"); - assertThat(latestAppVersion.version()).isEqualTo("v2.7.1.1"); + fun find_latest_app_version() { + val latestAppVersion = d2.settingModule().latestAppVersion().blockingGet() + Truth.assertThat(latestAppVersion?.version()).isEqualTo("40.2") + Truth.assertThat(latestAppVersion?.downloadURL()).isEqualTo( + "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.2/dhis2-40.2.apk" + ) } } \ No newline at end of file From a64130b611d7d69626768e665477300cba0c253e Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 10:50:49 +0100 Subject: [PATCH 169/222] [ANDROSDK-1816] Add unit tests --- .../core/settings/VersionsSettingsShould.kt | 48 +++++++ .../internal/LatestAppVersionCallShould.kt | 92 +++++++++++++ .../LatestAppVersionComparatorShould.kt | 125 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt create mode 100644 core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt new file mode 100644 index 0000000000..0b70eb4746 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.settings + +import com.google.common.truth.Truth +import org.hisp.dhis.android.core.common.BaseObjectShould +import org.hisp.dhis.android.core.common.ObjectShould +import org.junit.Test + +class VersionsSettingsShould : BaseObjectShould("settings/version.json"), ObjectShould { + + @Test + override fun map_from_json_string() { + val version = objectMapper.readValue(jsonStream, LatestAppVersion::class.java) + + Truth.assertThat(version.version()).isEqualTo("40.1") + Truth.assertThat(version.defaultVersion()).isTrue() + Truth.assertThat(version.userGroups()?.get(0)).isEqualTo("Kk12LkEWtXp") + Truth.assertThat(version.downloadURL()).isEqualTo( + "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", + ) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt new file mode 100644 index 0000000000..bb23fba917 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.settings.internal + +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutorMock +import org.hisp.dhis.android.core.maintenance.D2ErrorSamples +import org.hisp.dhis.android.core.settings.LatestAppVersion +import org.hisp.dhis.android.core.settings.ProgramSettings +import org.hisp.dhis.android.core.user.UserGroupCollectionRepository +import org.hisp.dhis.android.core.user.UserModule +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.stubbing.Answer + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(JUnit4::class) +class LatestAppVersionCallShould { + private val handler: LatestAppVersionHandler = mock() + private val service: SettingAppService = mock() + private val userModule: UserModule = mock() + private val versionComparator: LatestAppVersionComparator = mock() + private val versions: List = mock() + private val latestVersion: LatestAppVersion = mock() + private val userGroupCollectionRepository: UserGroupCollectionRepository = mock() + private val coroutineAPICallExecutor: CoroutineAPICallExecutorMock = CoroutineAPICallExecutorMock() + + private lateinit var latestAppVersionCall: LatestAppVersionCall + + @Before + fun setUp() { + whenVersionsAPICall { versions } + whenLatestVersionAPICall { latestVersion } + + whenever(userModule.userGroups()).thenReturn(userGroupCollectionRepository) + whenever(userGroupCollectionRepository.blockingGet()).thenReturn(emptyList()) + + latestAppVersionCall = LatestAppVersionCall(handler, service, userModule, versionComparator, coroutineAPICallExecutor) + } + + private fun whenVersionsAPICall(answer: Answer>) { + service.stub { + onBlocking { versions() }.doAnswer(answer) + } + } + + private fun whenLatestVersionAPICall(answer: Answer) { + service.stub { + onBlocking { latestAppVersion() }.doAnswer(answer) + } + } + + @Test + fun default_to_empty_collection_if_not_found() = runTest { + whenever(service.versions()) doAnswer { throw D2ErrorSamples.notFound() } + whenever(service.latestAppVersion()) doAnswer { throw D2ErrorSamples.notFound() } + + latestAppVersionCall.download(false) + + verify(handler).handleMany(emptyList()) + verifyNoMoreInteractions(handler) + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt new file mode 100644 index 0000000000..bee14a1dbd --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.settings.internal + +import com.google.common.truth.Truth.assertThat +import com.nhaarman.mockitokotlin2.* +import org.hisp.dhis.android.core.settings.LatestAppVersion +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + + +@RunWith(JUnit4::class) +class LatestAppVersionComparatorShould { + private val version1: LatestAppVersion = mock() + private val version2: LatestAppVersion = mock() + private val version3: LatestAppVersion = mock() + private val comparator = LatestAppVersionComparator().comparator + + + @Test + fun correctly_compare_versions_where_one_is_greater() { + whenever(version1.version()) doReturn "1.2.3" + whenever(version2.version()) doReturn "1.2.4" + + val result = comparator.compare(version1, version2) + + assertThat(result).isLessThan(0) + } + + @Test + fun treat_versions_as_equal_when_they_are_the_same() { + whenever(version1.version()) doReturn "1.2.3" + whenever(version2.version()) doReturn "1.2.3" + + val result = comparator.compare(version1, version2) + + assertThat(result).isEqualTo(0) + } + + @Test + fun handle_versions_with_different_lengths_correctly() { + whenever(version1.version()) doReturn "1.2" + whenever(version2.version()) doReturn "1.2.1" + + val result = comparator.compare(version1, version2) + + assertThat(result).isLessThan(0) + } + + @Test + fun handle_non_numeric_parts_by_treating_them_as_0() { + whenever(version1.version()) doReturn "1.2.x" + whenever(version2.version()) doReturn "1.2.1" + + val result = comparator.compare(version1, version2) + + assertThat(result).isLessThan(0) + } + + @Test + fun return_the_greatest_version() { + whenever(version1.version()) doReturn "1.2" + whenever(version2.version()) doReturn "1.1.1" + whenever(version3.version()) doReturn "1.1.0" + + val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) + + assertThat(highestVersion).isEqualTo(version1) + } + + @Test + fun return_the_first_in_list_when_two_greatest_versions() { + whenever(version1.version()) doReturn "1.2.0" + whenever(version2.version()) doReturn "1.2.1" + whenever(version3.version()) doReturn "1.2.1" + + val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) + + assertThat(highestVersion).isEqualTo(version2) + } + + @Test + fun return_the_first_version_when_string_is_not_a_number() { + whenever(version1.version()) doReturn "version_one" + whenever(version2.version()) doReturn "version_two" + whenever(version3.version()) doReturn "version_three" + + val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) + + assertThat(highestVersion).isEqualTo(version1) + } + + @Test + fun return_null_when_empty_list() { + val highestVersion = emptyList().maxWithOrNull(comparator) + + assertThat(highestVersion).isNull() + } +} From f67f122f55e174fb531f87fc5d02f3abbd363ea8 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 11:27:28 +0100 Subject: [PATCH 170/222] [ANDROSDK-1816] Return a payload instead of a list --- .../android/core/settings/internal/LatestAppVersionCall.kt | 2 +- .../dhis/android/core/settings/internal/SettingAppService.kt | 3 ++- .../hisp/dhis/android/core/settings/internal/SettingService.kt | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 5f88630eb4..0528fe3db1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -49,7 +49,7 @@ internal class LatestAppVersionCall( return coroutineAPICallExecutor.wrap(storeError = storeError) { val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } - val versions = settingAppService.versions() + val versions = settingAppService.versions().items() val filteredVersions = versions.filter { version -> version.userGroups()?.any { userGroupUid -> diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt index 61377a699e..a6585aa245 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt @@ -27,6 +27,7 @@ */ package org.hisp.dhis.android.core.settings.internal +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.settings.AnalyticsSettings import org.hisp.dhis.android.core.settings.AppearanceSettings import org.hisp.dhis.android.core.settings.DataSetSettings @@ -79,7 +80,7 @@ internal class SettingAppService( return settingService.latestAppVersion("$APK_DISTRIBUTION_NAMESPACE/latestVersion") } - suspend fun versions(): List { + suspend fun versions(): Payload { return settingService.versions("$APK_DISTRIBUTION_NAMESPACE/versions") } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt index 9a523ec623..4615037590 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.settings.internal import org.hisp.dhis.android.core.arch.api.fields.internal.Fields import org.hisp.dhis.android.core.arch.api.filters.internal.Which +import org.hisp.dhis.android.core.arch.api.payload.internal.Payload import org.hisp.dhis.android.core.settings.* import retrofit2.http.GET import retrofit2.http.Query @@ -71,5 +72,5 @@ internal interface SettingService { suspend fun latestAppVersion(@Url url: String): LatestAppVersion @GET - suspend fun versions(@Url url: String): List + suspend fun versions(@Url url: String): Payload } From 0e9e09d565027b0e118c8a748440ef849a7459e9 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 12:31:28 +0100 Subject: [PATCH 171/222] [ANDROSDK-1816] Add IgnoreBoolean column adapter --- .../hisp/dhis/android/core/settings/LatestAppVersion.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java index 3b48fca89d..3c9a737224 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java @@ -38,7 +38,9 @@ import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; +import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreBooleanColumnAdapter; import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreStringListColumnAdapter; +import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.CoreObject; import java.util.List; @@ -57,6 +59,7 @@ public abstract class LatestAppVersion implements CoreObject { @JsonProperty() @Nullable + @ColumnAdapter(IgnoreBooleanColumnAdapter.class) public abstract Boolean defaultVersion(); @JsonProperty() @@ -76,7 +79,7 @@ public static Builder builder() { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "") - public abstract static class Builder { + public abstract static class Builder extends BaseObject.Builder { public abstract Builder id(Long id); public abstract Builder version(String version); From 106930d22fb1e79110f7c9cd4b6a540f8bc939d7 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 12:59:22 +0100 Subject: [PATCH 172/222] [ANDROSDK-1816] Add data class ApkDistributionVersion to download versions --- ...onObjectRepositoryMockIntegrationShould.kt | 4 +- .../core/settings/LatestAppVersion.java | 19 -------- .../settings/internal/LatestAppVersionCall.kt | 9 ++-- .../internal/LatestAppVersionComparator.kt | 7 ++- .../settings/internal/SettingAppService.kt | 2 +- .../core/settings/internal/SettingService.kt | 2 +- .../core/settings/VersionsSettingsShould.kt | 11 ++--- .../internal/LatestAppVersionCallShould.kt | 5 ++- .../LatestAppVersionComparatorShould.kt | 45 +++++++++---------- 9 files changed, 42 insertions(+), 62 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt index 51d8d1ca11..af26ac97af 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/LatestAppVersionObjectRepositoryMockIntegrationShould.kt @@ -40,7 +40,7 @@ class LatestAppVersionObjectRepositoryMockIntegrationShould : BaseMockIntegratio val latestAppVersion = d2.settingModule().latestAppVersion().blockingGet() Truth.assertThat(latestAppVersion?.version()).isEqualTo("40.2") Truth.assertThat(latestAppVersion?.downloadURL()).isEqualTo( - "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.2/dhis2-40.2.apk" + "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.2/dhis2-40.2.apk", ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java index 3c9a737224..2ac7c4c570 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/LatestAppVersion.java @@ -35,16 +35,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; -import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreBooleanColumnAdapter; -import org.hisp.dhis.android.core.arch.db.adapters.ignore.internal.IgnoreStringListColumnAdapter; import org.hisp.dhis.android.core.common.BaseObject; import org.hisp.dhis.android.core.common.CoreObject; -import java.util.List; - @AutoValue @JsonDeserialize(builder = $$AutoValue_LatestAppVersion.Builder.class) public abstract class LatestAppVersion implements CoreObject { @@ -57,16 +52,6 @@ public abstract class LatestAppVersion implements CoreObject { @Nullable public abstract String downloadURL(); - @JsonProperty() - @Nullable - @ColumnAdapter(IgnoreBooleanColumnAdapter.class) - public abstract Boolean defaultVersion(); - - @JsonProperty() - @Nullable - @ColumnAdapter(IgnoreStringListColumnAdapter.class) - public abstract List userGroups(); - public static LatestAppVersion create(Cursor cursor) { return $AutoValue_LatestAppVersion.createFromCursor(cursor); } @@ -86,10 +71,6 @@ public abstract static class Builder extends BaseObject.Builder { public abstract Builder downloadURL(String downloadURL); - public abstract Builder defaultVersion(Boolean defaultVersion); - - public abstract Builder userGroups(List userGroups); - public abstract LatestAppVersion build(); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 0528fe3db1..5cb72b00d2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -45,21 +45,22 @@ internal class LatestAppVersionCall( ) : BaseSettingCall(coroutineAPICallExecutor) { override suspend fun tryFetch(storeError: Boolean): Result { - return coroutineAPICallExecutor.wrap(storeError = storeError) { val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } val versions = settingAppService.versions().items() val filteredVersions = versions.filter { version -> - version.userGroups()?.any { userGroupUid -> + version.userGroups?.any { userGroupUid -> userGroupUids.contains(userGroupUid) } ?: false } - val highestVersion = filteredVersions.maxWithOrNull(versionComparator.comparator) + val version = filteredVersions.maxWithOrNull(versionComparator.comparator) + ?: versions.find { it.defaultVersion == true } - highestVersion ?: versions.find { it.defaultVersion() == true } ?: settingAppService.latestAppVersion() + version?.let { LatestAppVersion.builder().version(it.version).downloadURL(it.downloadURL).build() } + ?: settingAppService.latestAppVersion() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt index 63112048b9..ff4a0caff5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparator.kt @@ -28,14 +28,13 @@ package org.hisp.dhis.android.core.settings.internal -import org.hisp.dhis.android.core.settings.LatestAppVersion import org.koin.core.annotation.Singleton @Singleton internal class LatestAppVersionComparator { - val comparator: Comparator = Comparator { v1, v2 -> - val partsV1 = v1.version()?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() - val partsV2 = v2.version()?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() + val comparator: Comparator = Comparator { v1, v2 -> + val partsV1 = v1.version?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() + val partsV2 = v2.version?.split(".")?.map { it.toIntOrNull() ?: 0 } ?: emptyList() var result = 0 val maxLength = maxOf(partsV1.size, partsV2.size) for (i in 0 until maxLength) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt index a6585aa245..e4a1912b85 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingAppService.kt @@ -80,7 +80,7 @@ internal class SettingAppService( return settingService.latestAppVersion("$APK_DISTRIBUTION_NAMESPACE/latestVersion") } - suspend fun versions(): Payload { + suspend fun versions(): Payload { return settingService.versions("$APK_DISTRIBUTION_NAMESPACE/versions") } diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt index 4615037590..46279b27b5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/SettingService.kt @@ -72,5 +72,5 @@ internal interface SettingService { suspend fun latestAppVersion(@Url url: String): LatestAppVersion @GET - suspend fun versions(@Url url: String): Payload + suspend fun versions(@Url url: String): Payload } diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt index 0b70eb4746..30e9489de3 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt @@ -30,18 +30,19 @@ package org.hisp.dhis.android.core.settings import com.google.common.truth.Truth import org.hisp.dhis.android.core.common.BaseObjectShould import org.hisp.dhis.android.core.common.ObjectShould +import org.hisp.dhis.android.core.settings.internal.ApkDistributionVersion import org.junit.Test class VersionsSettingsShould : BaseObjectShould("settings/version.json"), ObjectShould { @Test override fun map_from_json_string() { - val version = objectMapper.readValue(jsonStream, LatestAppVersion::class.java) + val version = objectMapper.readValue(jsonStream, ApkDistributionVersion::class.java) - Truth.assertThat(version.version()).isEqualTo("40.1") - Truth.assertThat(version.defaultVersion()).isTrue() - Truth.assertThat(version.userGroups()?.get(0)).isEqualTo("Kk12LkEWtXp") - Truth.assertThat(version.downloadURL()).isEqualTo( + Truth.assertThat(version.version).isEqualTo("40.1") + Truth.assertThat(version.defaultVersion).isTrue() + Truth.assertThat(version.userGroups?.get(0)).isEqualTo("Kk12LkEWtXp") + Truth.assertThat(version.downloadURL).isEqualTo( "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", ) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt index bb23fba917..b7088a3c0e 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCallShould.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.test.runTest import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallExecutorMock import org.hisp.dhis.android.core.maintenance.D2ErrorSamples import org.hisp.dhis.android.core.settings.LatestAppVersion -import org.hisp.dhis.android.core.settings.ProgramSettings import org.hisp.dhis.android.core.user.UserGroupCollectionRepository import org.hisp.dhis.android.core.user.UserModule import org.junit.Before @@ -64,7 +63,9 @@ class LatestAppVersionCallShould { whenever(userModule.userGroups()).thenReturn(userGroupCollectionRepository) whenever(userGroupCollectionRepository.blockingGet()).thenReturn(emptyList()) - latestAppVersionCall = LatestAppVersionCall(handler, service, userModule, versionComparator, coroutineAPICallExecutor) + latestAppVersionCall = LatestAppVersionCall( + handler, service, userModule, versionComparator, coroutineAPICallExecutor, + ) } private fun whenVersionsAPICall(answer: Answer>) { diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt index bee14a1dbd..abbbb5fcfb 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionComparatorShould.kt @@ -29,24 +29,21 @@ package org.hisp.dhis.android.core.settings.internal import com.google.common.truth.Truth.assertThat import com.nhaarman.mockitokotlin2.* -import org.hisp.dhis.android.core.settings.LatestAppVersion import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 - @RunWith(JUnit4::class) class LatestAppVersionComparatorShould { - private val version1: LatestAppVersion = mock() - private val version2: LatestAppVersion = mock() - private val version3: LatestAppVersion = mock() + private val version1: ApkDistributionVersion = mock() + private val version2: ApkDistributionVersion = mock() + private val version3: ApkDistributionVersion = mock() private val comparator = LatestAppVersionComparator().comparator - @Test fun correctly_compare_versions_where_one_is_greater() { - whenever(version1.version()) doReturn "1.2.3" - whenever(version2.version()) doReturn "1.2.4" + whenever(version1.version) doReturn "1.2.3" + whenever(version2.version) doReturn "1.2.4" val result = comparator.compare(version1, version2) @@ -55,8 +52,8 @@ class LatestAppVersionComparatorShould { @Test fun treat_versions_as_equal_when_they_are_the_same() { - whenever(version1.version()) doReturn "1.2.3" - whenever(version2.version()) doReturn "1.2.3" + whenever(version1.version) doReturn "1.2.3" + whenever(version2.version) doReturn "1.2.3" val result = comparator.compare(version1, version2) @@ -65,8 +62,8 @@ class LatestAppVersionComparatorShould { @Test fun handle_versions_with_different_lengths_correctly() { - whenever(version1.version()) doReturn "1.2" - whenever(version2.version()) doReturn "1.2.1" + whenever(version1.version) doReturn "1.2" + whenever(version2.version) doReturn "1.2.1" val result = comparator.compare(version1, version2) @@ -75,8 +72,8 @@ class LatestAppVersionComparatorShould { @Test fun handle_non_numeric_parts_by_treating_them_as_0() { - whenever(version1.version()) doReturn "1.2.x" - whenever(version2.version()) doReturn "1.2.1" + whenever(version1.version) doReturn "1.2.x" + whenever(version2.version) doReturn "1.2.1" val result = comparator.compare(version1, version2) @@ -85,9 +82,9 @@ class LatestAppVersionComparatorShould { @Test fun return_the_greatest_version() { - whenever(version1.version()) doReturn "1.2" - whenever(version2.version()) doReturn "1.1.1" - whenever(version3.version()) doReturn "1.1.0" + whenever(version1.version) doReturn "1.2" + whenever(version2.version) doReturn "1.1.1" + whenever(version3.version) doReturn "1.1.0" val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) @@ -96,9 +93,9 @@ class LatestAppVersionComparatorShould { @Test fun return_the_first_in_list_when_two_greatest_versions() { - whenever(version1.version()) doReturn "1.2.0" - whenever(version2.version()) doReturn "1.2.1" - whenever(version3.version()) doReturn "1.2.1" + whenever(version1.version) doReturn "1.2.0" + whenever(version2.version) doReturn "1.2.1" + whenever(version3.version) doReturn "1.2.1" val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) @@ -107,9 +104,9 @@ class LatestAppVersionComparatorShould { @Test fun return_the_first_version_when_string_is_not_a_number() { - whenever(version1.version()) doReturn "version_one" - whenever(version2.version()) doReturn "version_two" - whenever(version3.version()) doReturn "version_three" + whenever(version1.version) doReturn "version_one" + whenever(version2.version) doReturn "version_two" + whenever(version3.version) doReturn "version_three" val highestVersion = listOf(version1, version2, version3).maxWithOrNull(comparator) @@ -118,7 +115,7 @@ class LatestAppVersionComparatorShould { @Test fun return_null_when_empty_list() { - val highestVersion = emptyList().maxWithOrNull(comparator) + val highestVersion = emptyList().maxWithOrNull(comparator) assertThat(highestVersion).isNull() } From d2f4e86aaf4add8349556cb26c7eee73bdce3970 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 12:59:44 +0100 Subject: [PATCH 173/222] [ANDROSDK-1816] Refactor class name --- .../internal/ApkDistributionVersion.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt new file mode 100644 index 0000000000..5bfa99e6cd --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings.internal + +internal data class ApkDistributionVersion( + val version: String?, + val downloadURL: String?, + val defaultVersion: Boolean?, + val userGroups: List?, +) From 85a3119c70a6c3d99cf41713ace26364e54c8eaa Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Mon, 4 Mar 2024 13:12:05 +0100 Subject: [PATCH 174/222] [ANDROSDK-1812] Add support to export encrtyped database --- ...FromDatabaseAssetsMockIntegrationShould.kt | 17 +++++ .../core/arch/d2/internal/D2DIComponent.kt | 4 ++ .../core/arch/d2/internal/JavaDIClasses.kt | 1 - .../arch/db/access/internal/DatabaseExport.kt | 70 ++++++++++++++----- .../internal/DatabaseImportExportImpl.kt | 14 ++-- .../internal/MultiUserDatabaseManager.kt | 11 ++- 6 files changed, 92 insertions(+), 25 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt index ac179f0bb9..b75977a3d9 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportFromDatabaseAssetsMockIntegrationShould.kt @@ -148,11 +148,28 @@ class DatabaseImportExportFromDatabaseAssetsMockIntegrationShould : BaseMockInte @Test fun export_and_reimport() { + test_export_and_reimport(beforeExport = {}) + } + + @Test + fun export_and_reimport_encrypted() { + test_export_and_reimport(beforeExport = { + // Change encryption + d2.d2DIComponent.multiUserDatabaseManager.changeEncryptionIfRequired( + d2.d2DIComponent.credentialsSecureStore.get(), + encrypt = true, + ) + }) + } + + private fun test_export_and_reimport(beforeExport: () -> Unit) { d2.userModule().blockingLogIn(username, password, serverUrl) d2.metadataModule().blockingDownload() assertThat(d2.programModule().programs().blockingCount()).isEqualTo(3) + beforeExport() + val exportedFile = d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase() d2.userModule().accountManager().deleteCurrentAccount() diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponent.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponent.kt index 9b38382770..c281119023 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponent.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/D2DIComponent.kt @@ -33,6 +33,7 @@ import org.hisp.dhis.android.core.arch.api.executors.internal.CoroutineAPICallEx import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.storage.internal.* import org.hisp.dhis.android.core.category.internal.CategoryOptionStore +import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManager import org.hisp.dhis.android.core.configuration.internal.MultiUserDatabaseManagerForD2Manager import org.hisp.dhis.android.core.dataelement.internal.DataElementEndpointCallFactory import org.hisp.dhis.android.core.dataset.internal.DataSetEndpointCallFactory @@ -108,4 +109,7 @@ internal class D2DIComponent( @get:VisibleForTesting val interpreterSelector: InterpreterSelector, + + @get:VisibleForTesting + val multiUserDatabaseManager: MultiUserDatabaseManager, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt index 18ad25624b..8a3da2cc76 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/d2/internal/JavaDIClasses.kt @@ -30,7 +30,6 @@ package org.hisp.dhis.android.core.arch.d2.internal import org.hisp.dhis.android.core.arch.call.executors.internal.D2CallExecutor import org.hisp.dhis.android.core.arch.db.access.internal.DatabaseAdapterFactory -import org.hisp.dhis.android.core.arch.db.access.internal.DatabaseExport import org.hisp.dhis.android.core.configuration.internal.DatabaseEncryptionPasswordGenerator import org.hisp.dhis.android.core.configuration.internal.DatabaseEncryptionPasswordManager import org.hisp.dhis.android.core.maintenance.internal.ForeignKeyCleaner diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt index eccbad5545..839473e7cc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseExport.kt @@ -37,50 +37,84 @@ import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount import org.hisp.dhis.android.core.configuration.internal.DatabaseConfigurationHelper import org.hisp.dhis.android.core.configuration.internal.DatabaseEncryptionPasswordManager import org.koin.core.annotation.Singleton +import java.io.File @Singleton internal class DatabaseExport( private val context: Context, private val passwordManager: DatabaseEncryptionPasswordManager, - private val configurationHelper: DatabaseConfigurationHelper + private val configurationHelper: DatabaseConfigurationHelper, ) { fun encrypt(serverUrl: String, oldConfiguration: DatabaseAccount) { val newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration) export( - oldConfiguration, newConfiguration, null, - passwordManager.getPassword(newConfiguration.databaseName()), "Encrypt", null, - EncryptedDatabaseOpenHelper.hook + oldDatabaseFile = context.getDatabasePath(oldConfiguration.databaseName()), + newDatabaseFile = context.getDatabasePath(newConfiguration.databaseName()), + oldPassword = null, + newPassword = passwordManager.getPassword(newConfiguration.databaseName()), + tag = "Encrypt", + oldHook = null, + newHook = EncryptedDatabaseOpenHelper.hook, + ) + } + + fun encryptAndCopyTo(newConfiguration: DatabaseAccount, sourceFile: File, targetFile: File) { + export( + oldDatabaseFile = sourceFile, + newDatabaseFile = targetFile, + oldPassword = null, + newPassword = passwordManager.getPassword(newConfiguration.databaseName()), + tag = "Encrypt", + oldHook = null, + newHook = EncryptedDatabaseOpenHelper.hook, ) } fun decrypt(serverUrl: String, oldConfiguration: DatabaseAccount) { val newConfiguration = configurationHelper.changeEncryption(serverUrl, oldConfiguration) export( - oldConfiguration, newConfiguration, passwordManager.getPassword(oldConfiguration.databaseName()), - "", "Decrypt", EncryptedDatabaseOpenHelper.hook, null + oldDatabaseFile = context.getDatabasePath(oldConfiguration.databaseName()), + newDatabaseFile = context.getDatabasePath(newConfiguration.databaseName()), + oldPassword = passwordManager.getPassword(oldConfiguration.databaseName()), + newPassword = "", + tag = "Decrypt", + oldHook = EncryptedDatabaseOpenHelper.hook, + newHook = null, ) } + fun decryptAndCopyTo(account: DatabaseAccount, destinationFile: File) { + export( + oldDatabaseFile = context.getDatabasePath(account.databaseName()), + newDatabaseFile = destinationFile, + oldPassword = passwordManager.getPassword(account.databaseName()), + newPassword = "", + tag = "Decrypt", + oldHook = EncryptedDatabaseOpenHelper.hook, + newHook = null, + ) + } + + @Suppress("LongParameterList") private fun export( - oldConfiguration: DatabaseAccount, - newConfiguration: DatabaseAccount, + oldDatabaseFile: File, + newDatabaseFile: File, oldPassword: String?, newPassword: String, tag: String, oldHook: SQLiteDatabaseHook?, - newHook: SQLiteDatabaseHook? + newHook: SQLiteDatabaseHook?, ) { wrapAction({ - val oldDatabaseFile = context.getDatabasePath(oldConfiguration.databaseName()) - val newDatabaseFile = context.getDatabasePath(newConfiguration.databaseName()) - loadSQLCipher() val oldDatabase = SQLiteDatabase.openOrCreateDatabase(oldDatabaseFile, oldPassword, null, null, oldHook) oldDatabase.rawExecSQL( String.format( - "ATTACH DATABASE '%s' as alias KEY '%s';", newDatabaseFile.absolutePath, newPassword - ) + "ATTACH DATABASE '%s' as alias KEY '%s';", + newDatabaseFile.absolutePath, + newPassword, + ), ) if (newHook != null) { @@ -92,8 +126,11 @@ internal class DatabaseExport( val version = oldDatabase.version val newDatabase = SQLiteDatabase.openOrCreateDatabase( - newDatabaseFile, newPassword, null, - null, newHook + newDatabaseFile, + newPassword, + null, + null, + newHook, ) newDatabase.version = version @@ -102,6 +139,7 @@ internal class DatabaseExport( }, tag) } + @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown") private fun wrapAction(action: Action, tag: String) { val startMillis = System.currentTimeMillis() try { diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index 515ea74ac7..c3872562e0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -52,6 +52,7 @@ internal class DatabaseImportExportImpl( private val multiUserDatabaseManager: MultiUserDatabaseManager, private val userModule: UserModule, private val credentialsStore: CredentialsSecureStore, + private val databaseExport: DatabaseExport, ) : DatabaseImportExport { companion object { @@ -145,16 +146,15 @@ internal class DatabaseImportExportImpl( serverUrl = credentials.serverUrl, )!! + val databaseName = userConfiguration.databaseName() + val databaseFile = getDatabaseFile(databaseName) + if (userConfiguration.encrypted()) { - throw d2ErrorBuilder - .errorDescription("Database export of encrypted database not supported") - .errorCode(D2ErrorCode.DATABASE_EXPORT_ENCRYPTED_NOT_SUPPORTED) - .build() + databaseExport.decryptAndCopyTo(userConfiguration, copiedDatabase) + } else { + databaseFile.copyTo(copiedDatabase) } - val databaseName = userConfiguration.databaseName() - val databaseFile = getDatabaseFile(databaseName) - databaseFile.copyTo(copiedDatabase) CipherUtil.encryptFileUsingCredentials( input = copiedDatabase, output = protectedDatabase, diff --git a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt index df152a639a..1bcd5b6659 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/configuration/internal/MultiUserDatabaseManager.kt @@ -129,9 +129,16 @@ internal class MultiUserDatabaseManager( fun importAndLoadDb(account: DatabaseAccount, password: String) { val protectedDbPath = context.getDatabasePath(account.importDB()!!.protectedDbName()) val dbPath = context.getDatabasePath(account.databaseName()) + val tempDbPath = context.filesDir.resolve("temp.db").also { it.deleteIfExists() } try { - CipherUtil.decryptFileUsingCredentials(protectedDbPath, dbPath, account.username(), password) + CipherUtil.decryptFileUsingCredentials(protectedDbPath, tempDbPath, account.username(), password) protectedDbPath.deleteIfExists() + + if (account.encrypted()) { + databaseExport.encryptAndCopyTo(account, sourceFile = tempDbPath, targetFile = dbPath) + } else { + tempDbPath.copyTo(dbPath) + } databaseAdapterFactory.createOrOpenDatabase(databaseAdapter, account) val importedAccount = account.toBuilder() .importDB( @@ -144,6 +151,8 @@ internal class MultiUserDatabaseManager( } catch (e: Exception) { dbPath.deleteIfExists() throw e + } finally { + tempDbPath.deleteIfExists() } } From d875634c939e3d75a7515701685620a7b4e95ba2 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 4 Mar 2024 13:15:57 +0100 Subject: [PATCH 175/222] [ANDROSDK-1816] Remove IgnoreStringListColumnAdapter --- .../IgnoreStringListColumnAdapter.java | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java deleted file mode 100644 index b05d377115..0000000000 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/ignore/internal/IgnoreStringListColumnAdapter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2004-2023, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.arch.db.adapters.ignore.internal; - -import java.util.List; - -public final class IgnoreStringListColumnAdapter extends IgnoreColumnAdapter> { -} From 6ba2c4ee8fe075b5247c4ae95c169631aeabff65 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 5 Mar 2024 07:30:14 +0100 Subject: [PATCH 176/222] [ANDROSDK-1816] Rename defaultVersion to isDefault --- .../core/settings/internal/ApkDistributionVersion.kt | 2 +- .../android/core/settings/internal/LatestAppVersionCall.kt | 4 ++-- core/src/sharedTest/resources/settings/version.json | 2 +- core/src/sharedTest/resources/settings/versions.json | 6 +++--- .../dhis/android/core/settings/VersionsSettingsShould.kt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt index 5bfa99e6cd..7aced03d1f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/ApkDistributionVersion.kt @@ -31,6 +31,6 @@ package org.hisp.dhis.android.core.settings.internal internal data class ApkDistributionVersion( val version: String?, val downloadURL: String?, - val defaultVersion: Boolean?, + val isDefault: Boolean?, val userGroups: List?, ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt index 5cb72b00d2..368184040b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/LatestAppVersionCall.kt @@ -46,7 +46,7 @@ internal class LatestAppVersionCall( override suspend fun tryFetch(storeError: Boolean): Result { return coroutineAPICallExecutor.wrap(storeError = storeError) { - val userGroupUids = userModule.userGroups().blockingGet().map { it.uid() } + val userGroupUids = userModule.userGroups().blockingGetUids() val versions = settingAppService.versions().items() @@ -57,7 +57,7 @@ internal class LatestAppVersionCall( } val version = filteredVersions.maxWithOrNull(versionComparator.comparator) - ?: versions.find { it.defaultVersion == true } + ?: versions.find { it.isDefault == true } version?.let { LatestAppVersion.builder().version(it.version).downloadURL(it.downloadURL).build() } ?: settingAppService.latestAppVersion() diff --git a/core/src/sharedTest/resources/settings/version.json b/core/src/sharedTest/resources/settings/version.json index d39221b960..713e081e50 100644 --- a/core/src/sharedTest/resources/settings/version.json +++ b/core/src/sharedTest/resources/settings/version.json @@ -1,6 +1,6 @@ { "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", "version": "40.1", - "defaultVersion": true, + "isDefault": true, "userGroups": ["Kk12LkEWtXp"] } \ No newline at end of file diff --git a/core/src/sharedTest/resources/settings/versions.json b/core/src/sharedTest/resources/settings/versions.json index a08cd13005..92abe69e02 100644 --- a/core/src/sharedTest/resources/settings/versions.json +++ b/core/src/sharedTest/resources/settings/versions.json @@ -11,7 +11,7 @@ }, "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.3/dhis2-40.3.apk", "version": "40.3", - "defaultVersion": true + "isDefault": true }, { "androidOSVersion": { @@ -24,7 +24,7 @@ }, "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.2/dhis2-40.2.apk", "version": "40.2", - "defaultVersion": false, + "isDefault": false, "userGroups": ["Kk12LkEWtXp"] }, { @@ -38,7 +38,7 @@ }, "downloadURL": "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", "version": "40.1", - "defaultVersion": false, + "isDefault": false, "userGroups": ["Kk12LkEWtXp"] } ] diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt index 30e9489de3..e29babf4af 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/VersionsSettingsShould.kt @@ -40,7 +40,7 @@ class VersionsSettingsShould : BaseObjectShould("settings/version.json"), Object val version = objectMapper.readValue(jsonStream, ApkDistributionVersion::class.java) Truth.assertThat(version.version).isEqualTo("40.1") - Truth.assertThat(version.defaultVersion).isTrue() + Truth.assertThat(version.isDefault).isTrue() Truth.assertThat(version.userGroups?.get(0)).isEqualTo("Kk12LkEWtXp") Truth.assertThat(version.downloadURL).isEqualTo( "https://github.com/dhis2/dhis2-android-capture-app/releases/download/40.1/dhis2-40.1.apk", From 9f2831be1d9721d5c5578665941c6eddd26351cd Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 5 Mar 2024 07:54:14 +0100 Subject: [PATCH 177/222] [ANDOSDK-1825] Prevent isEventExpired failure on null eventDate --- .../hisp/dhis/android/core/event/internal/EventDateUtils.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt index dcaa926a3f..15daa9defd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt @@ -81,9 +81,11 @@ class EventDateUtils( false } + val eventDateOrDueDate = event.eventDate() ?: event.dueDate()!! + val expiredBecauseOfPeriod = programPeriodType?.let { periodType -> var nextPeriod = periodHelper - .blockingGetPeriodForPeriodTypeAndDate(periodType, event.eventDate()!!, 1).startDate()!! + .blockingGetPeriodForPeriodTypeAndDate(periodType, eventDateOrDueDate, 1).startDate()!! val currentDate: Date = getCalendar().time if (expiryDays > 0) { val calendar: Calendar = getCalendar() From 1a56f60cfeaf90cb597176987011b9016726fbe5 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 5 Mar 2024 08:01:58 +0100 Subject: [PATCH 178/222] [ANDROSDK-1814] Delete AnalytisDhisVisualization whose visualization is not downloaded --- ...ngObjectRepositoryMockIntegrationShould.kt | 1 + .../AnalyticsDhisVisualizationCleaner.kt | 49 +++++++++++++++++++ .../internal/TrackerVisualizationHandler.kt | 7 +++ .../internal/VisualizationHandler.kt | 7 +++ .../settings/analytics_settings_v3.json | 5 ++ 5 files changed, 69 insertions(+) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationCleaner.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt index dd183c732a..e2244d05b7 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/settings/AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould.kt @@ -46,6 +46,7 @@ class AnalyticsDhisVisualizationsSettingObjectRepositoryMockIntegrationShould : assertThat( analyticsDhisVisualizationsSetting.home().first().visualizations().first().name(), ).isNotEmpty() + assertThat(analyticsDhisVisualizationsSetting.home().first().visualizations().size).isEqualTo(3) assertThat(analyticsDhisVisualizationsSetting.program().size).isEqualTo(1) assertThat(analyticsDhisVisualizationsSetting.dataSet().size).isEqualTo(1) diff --git a/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationCleaner.kt b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationCleaner.kt new file mode 100644 index 0000000000..e6e4b022db --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/settings/internal/AnalyticsDhisVisualizationCleaner.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.settings.internal + +import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationTableInfo +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType +import org.koin.core.annotation.Singleton + +@Singleton +internal class AnalyticsDhisVisualizationCleaner( + private val store: AnalyticsDhisVisualizationStore, +) { + + fun deleteNotPresent(uids: List, type: AnalyticsDhisVisualizationType) { + val whereClause = WhereClauseBuilder() + .appendKeyStringValue(AnalyticsDhisVisualizationTableInfo.Columns.TYPE, type.name) + .appendNotInKeyStringValues(AnalyticsDhisVisualizationTableInfo.Columns.UID, uids) + .build() + + store.deleteWhere(whereClause) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt index ad25800f2c..65db11ccd8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandler.kt @@ -29,6 +29,8 @@ package org.hisp.dhis.android.core.visualization.internal import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType +import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationCleaner import org.hisp.dhis.android.core.visualization.LayoutPosition import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension @@ -38,6 +40,7 @@ import org.koin.core.annotation.Singleton internal class TrackerVisualizationHandler( store: TrackerVisualizationStore, private val trackerVisualizationCollectionCleaner: TrackerVisualizationCollectionCleaner, + private val analyticsDhisVisualizationCleaner: AnalyticsDhisVisualizationCleaner, private val dimensionHandler: TrackerVisualizationDimensionHandler, ) : IdentifiableHandlerImpl(store) { @@ -53,6 +56,10 @@ internal class TrackerVisualizationHandler( override fun afterCollectionHandled(oCollection: Collection?) { trackerVisualizationCollectionCleaner.deleteNotPresent(oCollection) + analyticsDhisVisualizationCleaner.deleteNotPresent( + uids = store.selectUids(), + type = AnalyticsDhisVisualizationType.TRACKER_VISUALIZATION, + ) } private fun toDimensions( diff --git a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandler.kt index 807d2339d9..5a5c802cec 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandler.kt @@ -29,6 +29,8 @@ package org.hisp.dhis.android.core.visualization.internal import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction import org.hisp.dhis.android.core.arch.handlers.internal.IdentifiableHandlerImpl +import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationType +import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationCleaner import org.hisp.dhis.android.core.visualization.LayoutPosition import org.hisp.dhis.android.core.visualization.Visualization import org.hisp.dhis.android.core.visualization.VisualizationDimension @@ -39,6 +41,7 @@ import org.koin.core.annotation.Singleton internal class VisualizationHandler( store: VisualizationStore, private val visualizationCollectionCleaner: VisualizationCollectionCleaner, + private val analyticsDhisVisualizationCleaner: AnalyticsDhisVisualizationCleaner, private val itemHandler: VisualizationDimensionItemHandler, ) : IdentifiableHandlerImpl(store) { @@ -55,6 +58,10 @@ internal class VisualizationHandler( override fun afterCollectionHandled(oCollection: Collection?) { visualizationCollectionCleaner.deleteNotPresent(oCollection) + analyticsDhisVisualizationCleaner.deleteNotPresent( + uids = store.selectUids(), + type = AnalyticsDhisVisualizationType.VISUALIZATION, + ) } private fun toItems( diff --git a/core/src/sharedTest/resources/settings/analytics_settings_v3.json b/core/src/sharedTest/resources/settings/analytics_settings_v3.json index b5bb0d22aa..d3f16ec339 100644 --- a/core/src/sharedTest/resources/settings/analytics_settings_v3.json +++ b/core/src/sharedTest/resources/settings/analytics_settings_v3.json @@ -82,6 +82,11 @@ "id": "s85urBIkN0z", "type": "TRACKER_VISUALIZATION", "timestamp": "2021-07-01T03:02:16.8770" + }, + { + "id": "invalid_uid", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T03:02:16.8770" } ] }, From ebac787a52ae28cab1ebfeef302d506fc43a2ee6 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Tue, 5 Mar 2024 10:45:30 +0100 Subject: [PATCH 179/222] [ANDOSDK-1825] Fix unit test --- .../hisp/dhis/android/core/event/internal/EventDateUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt index 15daa9defd..1a8515dc12 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt @@ -81,11 +81,11 @@ class EventDateUtils( false } - val eventDateOrDueDate = event.eventDate() ?: event.dueDate()!! + val eventDateOrDueDate = event.eventDate() ?: event.dueDate() val expiredBecauseOfPeriod = programPeriodType?.let { periodType -> var nextPeriod = periodHelper - .blockingGetPeriodForPeriodTypeAndDate(periodType, eventDateOrDueDate, 1).startDate()!! + .blockingGetPeriodForPeriodTypeAndDate(periodType, eventDateOrDueDate!!, 1).startDate()!! val currentDate: Date = getCalendar().time if (expiryDays > 0) { val calendar: Calendar = getCalendar() From 345ddaf274f6ae928414b7e7b5c545af77c043e3 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 5 Mar 2024 10:57:08 +0100 Subject: [PATCH 180/222] [ANDROSDK-1814] Adapt unit tests --- .../sharedTest/resources/settings/analytics_settings_v2.json | 5 +++++ .../dhis/android/core/settings/AnalyticsSettingAsserts.kt | 2 +- .../internal/TrackerVisualizationHandlerShould.kt | 3 +++ .../visualization/internal/VisualizationHandlerShould.kt | 3 +++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/src/sharedTest/resources/settings/analytics_settings_v2.json b/core/src/sharedTest/resources/settings/analytics_settings_v2.json index 5ded36f26b..f12d30035c 100644 --- a/core/src/sharedTest/resources/settings/analytics_settings_v2.json +++ b/core/src/sharedTest/resources/settings/analytics_settings_v2.json @@ -79,6 +79,11 @@ { "id": "s85urBIkN0z", "timestamp": "2021-07-01T03:02:16.8770" + }, + { + "id": "invalid_uid", + "type": "VISUALIZATION", + "timestamp": "2021-07-01T03:02:16.8770" } ] }, diff --git a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt index 4fa5972c72..3f105c1221 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/settings/AnalyticsSettingAsserts.kt @@ -99,7 +99,7 @@ object AnalyticsSettingAsserts { when (group.id()) { "12345678910" -> { assertThat(group.name()).isEqualTo("Ejemplo") - assertThat(group.visualizations().size).isEqualTo(3) + assertThat(group.visualizations().size).isEqualTo(4) } "12345678911" -> { assertThat(group.name()).isEqualTo("Otro ejemplo") diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt index 4690a890fb..3160c8c03d 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/TrackerVisualizationHandlerShould.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.visualization.internal import com.nhaarman.mockitokotlin2.* import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationCleaner import org.hisp.dhis.android.core.visualization.TrackerVisualization import org.hisp.dhis.android.core.visualization.TrackerVisualizationDimension import org.junit.Before @@ -42,6 +43,7 @@ class TrackerVisualizationHandlerShould { private val store: TrackerVisualizationStore = mock() private val collectionCleaner: TrackerVisualizationCollectionCleaner = mock() private val dimensionHandler: TrackerVisualizationDimensionHandler = mock() + private val analyticsDhisVisualizationCleaner: AnalyticsDhisVisualizationCleaner = mock() private val dimension: TrackerVisualizationDimension = TrackerVisualizationDimension.builder().build() private val trackerVisualization: TrackerVisualization = mock() @@ -53,6 +55,7 @@ class TrackerVisualizationHandlerShould { trackerVisualizationHandler = TrackerVisualizationHandler( store, collectionCleaner, + analyticsDhisVisualizationCleaner, dimensionHandler, ) diff --git a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandlerShould.kt index f1e2e2dbbe..09339bb3ce 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/visualization/internal/VisualizationHandlerShould.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.visualization.internal import com.nhaarman.mockitokotlin2.* import org.hisp.dhis.android.core.arch.handlers.internal.HandleAction +import org.hisp.dhis.android.core.settings.internal.AnalyticsDhisVisualizationCleaner import org.hisp.dhis.android.core.visualization.Visualization import org.hisp.dhis.android.core.visualization.VisualizationDimension import org.junit.Before @@ -41,6 +42,7 @@ class VisualizationHandlerShould { private val visualizationStore: VisualizationStore = mock() private val visualizationCollectionCleaner: VisualizationCollectionCleaner = mock() + private val analyticsDhisVisualizationCleaner: AnalyticsDhisVisualizationCleaner = mock() private val visualizationDimensionItemHandler: VisualizationDimensionItemHandler = mock() private val visualizationDimension: VisualizationDimension = mock() private val visualization: Visualization = mock() @@ -53,6 +55,7 @@ class VisualizationHandlerShould { visualizationHandler = VisualizationHandler( visualizationStore, visualizationCollectionCleaner, + analyticsDhisVisualizationCleaner, visualizationDimensionItemHandler, ) From cff0213ea68e366a9b6bda14d61698901145cce9 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Tue, 5 Mar 2024 15:43:18 +0100 Subject: [PATCH 181/222] [ANDROSDK-1814] Adapt integration tests --- .../hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java index f540c92aa6..66da85347d 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java +++ b/core/src/main/java/org/hisp/dhis/android/core/mockwebserver/Dhis2MockServer.java @@ -380,6 +380,7 @@ public void enqueueMetadataResponses() { enqueueMockResponse(CATEGORY_OPTION_ORGUNITS_JSON); enqueueMockResponse(VISUALIZATIONS_1_JSON); enqueueMockResponse(VISUALIZATIONS_2_JSON); + enqueueMockResponse(404); enqueueMockResponse(TRACKER_VISUALIZATIONS_1_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); enqueueMockResponse(PROGRAMS_INDICATORS_JSON); From 83e99fbc43397fe4f98a863a7f431ac8aa99e4f0 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 6 Mar 2024 10:07:45 +0100 Subject: [PATCH 182/222] [ANDROSDK-1828] TrackerLineList: keep columns order --- .../internal/TrackerLineListParams.kt | 13 +++--- .../internal/TrackerLineListRepositoryImpl.kt | 4 +- .../android/core/util/CollectionExtensions.kt | 43 +++++++++++++++++++ .../TrackerLineListRepositoryShould.kt | 22 +++++++--- .../internal/TrackerLineListParamsShould.kt | 2 +- 5 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/util/CollectionExtensions.kt diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index 3b7db17076..8e3f1a130c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.util.replaceOrPush internal data class TrackerLineListParams( val trackerVisualization: String?, @@ -46,23 +47,23 @@ internal data class TrackerLineListParams( programId = other.programId ?: programId, programStageId = other.programStageId ?: programStageId, ).run { - other.columns.fold(this) { params, item -> params.pushToColumns(item) } + other.columns.fold(this) { params, item -> params.updateInColumns(item) } }.run { - other.filters.fold(this) { params, item -> params.pushToFilter(item) } + other.filters.fold(this) { params, item -> params.updateInFilters(item) } } } - fun pushToColumns(item: TrackerLineListItem): TrackerLineListParams { + fun updateInColumns(item: TrackerLineListItem): TrackerLineListParams { return copy( - columns = columns.filterNot { it.id == item.id } + item, + columns = columns.replaceOrPush(item) { it.id == item.id }, filters = filters.filterNot { it.id == item.id }, ) } - fun pushToFilter(item: TrackerLineListItem): TrackerLineListParams { + fun updateInFilters(item: TrackerLineListItem): TrackerLineListParams { return copy( columns = columns.filterNot { it.id == item.id }, - filters = filters.filterNot { it.id == item.id } + item, + filters = filters.replaceOrPush(item) { it.id == item.id }, ) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index 3c65f41527..f5559f939c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -62,11 +62,11 @@ internal class TrackerLineListRepositoryImpl( } override fun withColumn(column: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.pushToColumns(column) } + return updateParams { params.updateInColumns(column) } } override fun withFilter(filter: TrackerLineListItem): TrackerLineListRepositoryImpl { - return updateParams { params.pushToFilter(filter) } + return updateParams { params.updateInFilters(filter) } } override fun withTrackerVisualization(trackerVisualization: String): TrackerLineListRepositoryImpl { diff --git a/core/src/main/java/org/hisp/dhis/android/core/util/CollectionExtensions.kt b/core/src/main/java/org/hisp/dhis/android/core/util/CollectionExtensions.kt new file mode 100644 index 0000000000..49423ea57d --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/util/CollectionExtensions.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.util + +internal fun Collection.replace(index: Int, element: E): List { + return this.mapIndexed { i, item -> if (i == index) element else item } +} + +internal fun Collection.replaceOrPush(element: E, predicate: (E) -> Boolean): List { + val itemIndex = this.indexOfFirst(predicate) + + return if (itemIndex >= 0) { + this.replace(itemIndex, element) + } else { + this + element + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt index 3f0a2b82d9..1b0cc6f875 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt @@ -50,15 +50,15 @@ class TrackerLineListRepositoryShould { private val repository = TrackerLineListRepositoryImpl(initialParams, service) @Test - fun `Call service with overridden columns`() { + fun `Call service with overridden columns respecting initial order`() { val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "program", "programStage", listOf(), null) - val de1_2 = de1_1.copy(filters = listOf(DataFilter.EqualTo("value"))) val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "program", "programStage", listOf(), null) + val de1_2 = de1_1.copy(filters = listOf(DataFilter.EqualTo("value"))) repository .withColumn(de1_1) - .withColumn(de1_2) .withColumn(de2_1) + .withColumn(de1_2) .blockingEvaluate() verify(service).evaluate(paramsCaptor.capture()) @@ -67,10 +67,18 @@ class TrackerLineListRepositoryShould { assertThat(columns.size).isEqualTo(2) val dataElementColumns = columns.filterIsInstance() - val de1 = dataElementColumns.find { it.dataElement == "dataElement1" }!! - val de2 = dataElementColumns.find { it.dataElement == "dataElement2" }!! - assertThat(de1.filters.size).isEqualTo(1) - assertThat(de2.filters.size).isEqualTo(0) + dataElementColumns.forEachIndexed { index, item -> + when (index) { + 0 -> { + assertThat(item.dataElement).isEqualTo("dataElement1") + assertThat(item.filters.size).isEqualTo(1) + } + 1 -> { + assertThat(item.dataElement).isEqualTo("dataElement2") + assertThat(item.filters.size).isEqualTo(0) + } + } + } } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt index 0f5cf08c65..c88fa16d0b 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt @@ -73,8 +73,8 @@ class TrackerLineListParamsShould { assertThat(params.programId).isEqualTo("program_uid") assertThat(params.programStageId).isEqualTo("program_stage_uid") assertThat(params.columns).containsExactly( - TrackerLineListItem.ProgramIndicator("indicator"), TrackerLineListItem.ProgramAttribute("attribute", listOf(DataFilter.NotEqualTo("10"))), + TrackerLineListItem.ProgramIndicator("indicator"), ) assertThat(params.filters).containsExactly( TrackerLineListItem.EventDate(listOf(DateFilter.Absolute("202405"))), From 9b73bc3200165314512e147cea970da38a37ae58 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 6 Mar 2024 11:36:27 +0100 Subject: [PATCH 183/222] [ANDROSDK-1832] Add method in Dateutils to get the current time and date --- .../dhis/android/core/arch/helpers/DateUtils.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/DateUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/DateUtils.kt index eef30bd5eb..0c4ae84d92 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/DateUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/DateUtils.kt @@ -27,6 +27,9 @@ */ package org.hisp.dhis.android.core.arch.helpers +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import org.hisp.dhis.android.core.arch.dateformat.internal.SafeDateFormat import org.hisp.dhis.android.core.period.Period import org.hisp.dhis.android.core.period.PeriodType @@ -95,4 +98,18 @@ object DateUtils { c.add(Calendar.MONTH, amount) return c.time } + + private fun Int.zeroPrefixed(length: Int = 2): String = this.toString().padStart(length, '0') + internal fun getCurrentTimeAndDate(): String { + val dateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + + val year = dateTime.year + val month = dateTime.monthNumber.zeroPrefixed() + val day = dateTime.dayOfMonth.zeroPrefixed() + val hour = dateTime.hour.zeroPrefixed() + val minute = dateTime.minute.zeroPrefixed() + val seconds = dateTime.second.zeroPrefixed() + + return "$year$month$day-$hour$minute$seconds" + } } From b6d95e7df1c153fc0301b7e4148979cc5149b07e Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Wed, 6 Mar 2024 11:55:44 +0100 Subject: [PATCH 184/222] [ANDROSDK-1832] Improve db zip name generation --- .../arch/db/access/internal/DatabaseImportExportImpl.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt index c3872562e0..07a7bc2ce7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/access/internal/DatabaseImportExportImpl.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.arch.db.access.internal import android.content.Context import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata import org.hisp.dhis.android.core.arch.db.access.DatabaseImportExport +import org.hisp.dhis.android.core.arch.helpers.DateUtils.getCurrentTimeAndDate import org.hisp.dhis.android.core.arch.json.internal.ObjectMapperFactory.objectMapper import org.hisp.dhis.android.core.arch.storage.internal.CredentialsSecureStore import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount @@ -59,7 +60,7 @@ internal class DatabaseImportExportImpl( const val ExportDatabase = "export-database.db" const val ExportDatabaseProtected = "export-database-protected.db" const val ExportMetadata = "export-metadata.json" - const val ExportZip = "export-database.zip" + const val ExportZip = "-database.zip" } private val d2ErrorBuilder = D2Error.builder() @@ -131,7 +132,6 @@ internal class DatabaseImportExportImpl( val exportMetadataFile = getWorkingDir().resolve(ExportMetadata).also { it.deleteIfExists() } val copiedDatabase = getWorkingDir().resolve(ExportDatabase).also { it.deleteIfExists() } val protectedDatabase = getWorkingDir().resolve(ExportDatabaseProtected).also { it.deleteIfExists() } - val zipFile = getWorkingDir().resolve(ExportZip).also { it.deleteIfExists() } if (!userModule.blockingIsLogged()) { throw d2ErrorBuilder @@ -174,6 +174,9 @@ internal class DatabaseImportExportImpl( it.write(objectMapper().writeValueAsString(metadata)) } + val zipName = credentials.username + '-' + getCurrentTimeAndDate() + '-' + ExportZip + val zipFile = getWorkingDir().resolve(zipName).also { it.deleteIfExists() } + FileUtils.zipFiles( files = listOf(exportMetadataFile, protectedDatabase), zipFile = zipFile, From d6c89ba746059f1bbe8d43735834a92d79288895 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 6 Mar 2024 11:42:50 +0100 Subject: [PATCH 185/222] [ANDROSDK-1831] TrackerLineList: make dataElement config more robust --- ...rackerLineListRepositoryEvaluatorShould.kt | 7 ++-- .../internal/AnalyticsModelHelper.kt | 4 +-- .../trackerlinelist/TrackerLineListModel.kt | 7 ++-- .../TrackerLineListRepository.kt | 2 +- .../internal/TrackerLineListParams.kt | 16 +++------ .../internal/TrackerLineListRepositoryImpl.kt | 3 +- .../internal/TrackerLineListService.kt | 1 - .../TrackerLineListServiceMetadataHelper.kt | 12 +++---- .../internal/TrackerVisualizationMapper.kt | 34 +++++++++++-------- .../TrackerLineListRepositoryShould.kt | 4 +-- .../internal/TrackerLineListParamsShould.kt | 7 +++- 11 files changed, 47 insertions(+), 50 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 1e72c1c17b..4b65463c4c 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -103,7 +103,6 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati .withColumn( TrackerLineListItem.ProgramDataElement( dataElement = dataElement1.uid(), - program = program.uid(), programStage = programStage1.uid(), filters = listOf( DataFilter.GreaterThan("15"), @@ -203,7 +202,7 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati ) val result = d2.analyticsModule().trackerLineList() - .withEventOutput(program.uid(), programStage1.uid()) + .withEventOutput(programStage1.uid()) .withColumn(TrackerLineListItem.EventDate()) .withColumn(TrackerLineListItem.ProgramIndicator(programIndicator)) .blockingEvaluate() @@ -262,8 +261,8 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati helper.insertTrackedEntityDataValue(event2, dataElement1.uid(), "10") val result = d2.analyticsModule().trackerLineList() - .withEventOutput(program.uid(), programStage1.uid()) - .withColumn(TrackerLineListItem.ProgramDataElement(dataElement1.uid(), program.uid(), programStage1.uid())) + .withEventOutput(programStage1.uid()) + .withColumn(TrackerLineListItem.ProgramDataElement(dataElement1.uid(), programStage1.uid())) .blockingEvaluate() val rows = result.getOrThrow().rows diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt index da82998d9c..030ccbb187 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/internal/AnalyticsModelHelper.kt @@ -29,7 +29,7 @@ package org.hisp.dhis.android.core.analytics.internal internal object AnalyticsModelHelper { - fun eventDataElementId(program: String?, programStage: String?, dataElement: String): String { - return listOfNotNull(program, programStage, dataElement).joinToString(".") + fun eventDataElementId(programStage: String, dataElement: String): String { + return "$programStage.$dataElement" } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 6ded9a7afb..3f8c6f5b35 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -62,16 +62,15 @@ sealed class TrackerLineListItem(val id: String) { data class ProgramDataElement( val dataElement: String, - val program: String?, - val programStage: String?, + val programStage: String, val filters: List = emptyList(), val repetitionIndexes: List? = null, ) : TrackerLineListItem( - eventDataElementId(program, programStage, dataElement) + + eventDataElementId(programStage, dataElement) + (repetitionIndexes?.joinToString { it.toString() } ?: ""), ) { - val stageDataElementIdx = eventDataElementId(program, programStage, dataElement) + val stageDataElementIdx = eventDataElementId(programStage, dataElement) } data class ProgramStatusItem(val filters: List = emptyList()) : diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt index 3f8396745b..88467d8f87 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -34,7 +34,7 @@ import org.hisp.dhis.android.core.arch.helpers.Result interface TrackerLineListRepository { - fun withEventOutput(programId: String, programStageId: String?): TrackerLineListRepository + fun withEventOutput(programStageId: String): TrackerLineListRepository fun withEnrollmentOutput(programId: String): TrackerLineListRepository diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index 8e3f1a130c..76eeb19c10 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -88,18 +88,10 @@ internal data class TrackerLineListParams( } private fun flattenDataElement(item: TrackerLineListItem.ProgramDataElement): List { - val flattenDataElements = - if (item.repetitionIndexes.isNullOrEmpty()) { - listOf(item) - } else { - sortIndexes(item.repetitionIndexes).map { idx -> item.copy(repetitionIndexes = listOf(idx)) } - } - - return flattenDataElements.map { - it.copy( - program = it.program ?: programId, - programStage = it.programStage ?: programStageId, - ) + return if (item.repetitionIndexes.isNullOrEmpty()) { + listOf(item) + } else { + sortIndexes(item.repetitionIndexes).map { idx -> item.copy(repetitionIndexes = listOf(idx)) } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index f5559f939c..292c219cd1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -42,11 +42,10 @@ internal class TrackerLineListRepositoryImpl( private val service: TrackerLineListService, ) : TrackerLineListRepository { - override fun withEventOutput(programId: String, programStageId: String?): TrackerLineListRepositoryImpl { + override fun withEventOutput(programStageId: String): TrackerLineListRepositoryImpl { return updateParams { params.copy( outputType = TrackerLineListOutputType.EVENT, - programId = programId, programStageId = programStageId, ) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index a9d968cc99..c25cffb12f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -126,7 +126,6 @@ internal class TrackerLineListService( "" } + "WHERE " + - "$EventAlias.${EventTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + "${getEventWhereClause(params, context)} " } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt index 1c36394d17..7205f2a82b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListServiceMetadataHelper.kt @@ -100,14 +100,14 @@ internal class TrackerLineListServiceMetadataHelper( ?.let { MetadataItem.DataElementItem(it) } ?: throw AnalyticsException.InvalidDataElement(item.dataElement) - val program = item.program?.let { getProgram(it) } - ?.let { MetadataItem.ProgramItem(it) } - ?: throw AnalyticsException.InvalidArguments("DataElement ${item.dataElement} has no program defined") + val programStage = getProgramStage(item.programStage) + .let { MetadataItem.ProgramStageItem(it) } - val programStage = item.programStage?.let { getProgramStage(it) } - ?.let { MetadataItem.ProgramStageItem(it) } + val program = programStage.item.program()?.uid()?.let { getProgram(it) } + ?.let { MetadataItem.ProgramItem(it) } + ?: throw AnalyticsException.InvalidArguments("ProgramStage ${programStage.item.uid()} has no program") - return listOfNotNull(dataElement, program, programStage) + return listOf(dataElement, program, programStage) } private fun getProgram(programId: String): Program { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index de1e07c189..0d5873952f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -60,8 +60,8 @@ internal class TrackerVisualizationMapper( programId = trackerVisualization.program()?.uid(), programStageId = trackerVisualization.programStage()?.uid(), outputType = mapOutputType(trackerVisualization.outputType()), - columns = mapDimensions(trackerVisualization.columns()), - filters = mapDimensions(trackerVisualization.filters()), + columns = mapDimensions(trackerVisualization.columns(), trackerVisualization), + filters = mapDimensions(trackerVisualization.filters(), trackerVisualization), ) } @@ -73,21 +73,23 @@ internal class TrackerVisualizationMapper( } } - private fun mapDimensions(dimensions: List?): List { + private fun mapDimensions( + dimensions: List?, + trackerVisualization: TrackerVisualization, + ): List { return dimensions?.mapNotNull { item -> - val mapper = when (item.dimensionType()) { - "ORGANISATION_UNIT" -> ::mapOrganisationUnit - "PERIOD" -> ::mapPeriod - "PROGRAM_INDICATOR" -> ::mapProgramIndicator - "PROGRAM_ATTRIBUTE" -> ::mapProgramAttribute - "PROGRAM_DATA_ELEMENT" -> ::mapProgramDataElement - "DATA_X" -> ::mapDataX + when (item.dimensionType()) { + "ORGANISATION_UNIT" -> mapOrganisationUnit(item) + "PERIOD" -> mapPeriod(item) + "PROGRAM_INDICATOR" -> mapProgramIndicator(item) + "PROGRAM_ATTRIBUTE" -> mapProgramAttribute(item) + "PROGRAM_DATA_ELEMENT" -> mapProgramDataElement(item, trackerVisualization) + "DATA_X" -> mapDataX(item) "ORGANISATION_UNIT_GROUP_SET" -> throw AnalyticsException.InvalidArguments("Dimension ORGANISATION_UNIT_GROUP_SET IS not supported") - else -> { _ -> null } + else -> null } - mapper(item) } ?: emptyList() } @@ -149,12 +151,14 @@ internal class TrackerVisualizationMapper( } } - private fun mapProgramDataElement(item: TrackerVisualizationDimension): TrackerLineListItem? { + private fun mapProgramDataElement( + item: TrackerVisualizationDimension, + trackerVisualization: TrackerVisualization, + ): TrackerLineListItem? { return item.dimension()?.let { uid -> TrackerLineListItem.ProgramDataElement( uid, - item.program()?.uid(), - item.programStage()?.uid(), + item.programStage()?.uid() ?: trackerVisualization.programStage()!!.uid(), mapDataFilters(item), item.repetition()?.indexes(), ) diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt index 1b0cc6f875..e38d7e63ba 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryShould.kt @@ -51,8 +51,8 @@ class TrackerLineListRepositoryShould { @Test fun `Call service with overridden columns respecting initial order`() { - val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "program", "programStage", listOf(), null) - val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "program", "programStage", listOf(), null) + val de1_1 = TrackerLineListItem.ProgramDataElement("dataElement1", "programStage", listOf(), null) + val de2_1 = TrackerLineListItem.ProgramDataElement("dataElement2", "programStage", listOf(), null) val de1_2 = de1_1.copy(filters = listOf(DataFilter.EqualTo("value"))) repository diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt index c88fa16d0b..9ec80ad058 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParamsShould.kt @@ -89,7 +89,12 @@ class TrackerLineListParamsShould { programId = "programId", programStageId = null, columns = listOf( - TrackerLineListItem.ProgramDataElement("dataElement", null, null, listOf(), listOf(0, -1, -2, 1, 2)), + TrackerLineListItem.ProgramDataElement( + "dataElement", + "programStage", + listOf(), + listOf(0, -1, -2, 1, 2), + ), ), filters = emptyList(), ) From 1e734c462993a021fc7cf7ee394b8b0132c0b665 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 7 Mar 2024 11:23:59 +0100 Subject: [PATCH 186/222] [ANDROSDK-1830] TrackerLineList: add like and equal operators to all items --- ...rackerLineListRepositoryEvaluatorShould.kt | 26 +++++++- .../trackerlinelist/TrackerLineListModel.kt | 34 ++++++++++- .../internal/TrackerVisualizationMapper.kt | 5 ++ .../internal/evaluator/BaseDateEvaluator.kt | 11 ++++ .../internal/evaluator/BaseEnumEvaluator.kt | 60 +++++++++++++++++++ .../{ => evaluator}/DataFilterHelper.kt | 37 ++++++------ .../evaluator/EventStatusEvaluator.kt | 11 ++-- .../internal/evaluator/FilterHelper.kt | 48 +++++++++++++++ .../evaluator/OrganisationUnitEvaluator.kt | 11 ++++ .../evaluator/ProgramAttributeEvaluator.kt | 1 - .../evaluator/ProgramDataElementEvaluator.kt | 1 - .../evaluator/ProgramIndicatorEvaluator.kt | 1 - .../evaluator/ProgramStatusEvaluator.kt | 9 +-- 13 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt rename core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/{ => evaluator}/DataFilterHelper.kt (62%) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 4b65463c4c..003852f2ea 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -35,6 +35,7 @@ import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEv import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.dataElement1 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.generator import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild1 +import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.orgunitChild2 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201911 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period201912 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.period202001 @@ -67,7 +68,7 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati val result = d2.analyticsModule().trackerLineList() .withEnrollmentOutput(program.uid()) - .withColumn(TrackerLineListItem.OrganisationUnitItem(filters = emptyList())) + .withColumn(TrackerLineListItem.OrganisationUnitItem()) .withColumn( TrackerLineListItem.ProgramAttribute( uid = attribute1.uid(), @@ -250,6 +251,29 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati evaluateExpression("if(containsItems(A{${attribute2.uid()}}, 'xy', 'ALLERGY'), '1', '0')", "0") } + @Test + fun evaluate_orgunit_filters() { + val event1 = generator.generate() + helper.createSingleEvent(event1, program.uid(), programStage1.uid(), orgunitChild1.uid()) + val event2 = generator.generate() + helper.createSingleEvent(event2, program.uid(), programStage1.uid(), orgunitChild2.uid()) + + val result = d2.analyticsModule().trackerLineList() + .withEventOutput(program.uid(), programStage1.uid()) + .withColumn( + TrackerLineListItem.OrganisationUnitItem( + filters = listOf( + OrganisationUnitFilter.Like(orgunitChild1.displayName()!!), + ), + ), + ) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(1) + assertThat(rows.first().first().value).isEqualTo(orgunitChild1.displayName()) + } + @Test fun evaluate_single_events() { val event1 = generator.generate() diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 3f8c6f5b35..c663f9c5f1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -36,7 +36,7 @@ import org.hisp.dhis.android.core.event.EventStatus sealed class TrackerLineListItem(val id: String) { - data class OrganisationUnitItem(val filters: List) : + data class OrganisationUnitItem(val filters: List = emptyList()) : TrackerLineListItem(Label.OrganisationUnit) data class LastUpdated(override val filters: List = emptyList()) : @@ -73,10 +73,10 @@ sealed class TrackerLineListItem(val id: String) { val stageDataElementIdx = eventDataElementId(programStage, dataElement) } - data class ProgramStatusItem(val filters: List = emptyList()) : + data class ProgramStatusItem(val filters: List> = emptyList()) : TrackerLineListItem(Label.ProgramStatus) - data class EventStatusItem(val filters: List = emptyList()) : + data class EventStatusItem(val filters: List> = emptyList()) : TrackerLineListItem(Label.EventStatus) object CreatedBy : TrackerLineListItem(Label.CreatedBy) @@ -94,12 +94,40 @@ sealed class OrganisationUnitFilter { data class Relative(val relative: RelativeOrganisationUnit) : OrganisationUnitFilter() data class Level(val uid: String) : OrganisationUnitFilter() data class Group(val uid: String) : OrganisationUnitFilter() + data class EqualTo(val orgunitName: String) : OrganisationUnitFilter() + data class NotEqualTo(val orgunitName: String) : OrganisationUnitFilter() + data class EqualToIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() + data class NotEqualToIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() + data class Like(val orgunitName: String) : OrganisationUnitFilter() + data class NotLike(val orgunitName: String) : OrganisationUnitFilter() + data class LikeIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() + data class NotLikeIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() } sealed class DateFilter { data class Relative(val relative: RelativePeriod) : DateFilter() data class Absolute(val uid: String) : DateFilter() data class Range(val startDate: String, val endDate: String) : DateFilter() + data class EqualTo(val timestamp: String) : DateFilter() + data class NotEqualTo(val timestamp: String) : DateFilter() + data class EqualToIgnoreCase(val timestamp: String) : DateFilter() + data class NotEqualToIgnoreCase(val timestamp: String) : DateFilter() + data class Like(val timestamp: String) : DateFilter() + data class NotLike(val timestamp: String) : DateFilter() + data class LikeIgnoreCase(val timestamp: String) : DateFilter() + data class NotLikeIgnoreCase(val timestamp: String) : DateFilter() +} + +sealed class EnumFilter { + data class EqualTo(val value: String) : EnumFilter() + data class NotEqualTo(val value: String) : EnumFilter() + data class EqualToIgnoreCase(val value: String) : EnumFilter() + data class NotEqualToIgnoreCase(val value: String) : EnumFilter() + data class Like(val value: String) : EnumFilter() + data class NotLike(val value: String) : EnumFilter() + data class LikeIgnoreCase(val value: String) : EnumFilter() + data class NotLikeIgnoreCase(val value: String) : EnumFilter() + data class In(val values: List) : EnumFilter() } sealed class DataFilter { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index 0d5873952f..591be13ba0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -35,6 +35,7 @@ import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.orgunitLevel import org.hisp.dhis.android.core.analytics.internal.AnalyticsRegex.uidRegex import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.EnumFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.OrganisationUnitFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.arch.db.querybuilders.internal.WhereClauseBuilder @@ -171,11 +172,15 @@ internal class TrackerVisualizationMapper( "lastUpdatedBy" -> TrackerLineListItem.LastUpdatedBy "programStatus" -> TrackerLineListItem.ProgramStatusItem( filters = item.items()?.mapNotNull { e -> EnrollmentStatus.entries.find { it.name == e.uid() } } + .takeIf { !it.isNullOrEmpty() } + ?.let { statuses -> listOf(EnumFilter.In(statuses)) } ?: emptyList(), ) "eventStatus" -> TrackerLineListItem.EventStatusItem( filters = item.items()?.mapNotNull { e -> EventStatus.entries.find { it.name == e.uid() } } + .takeIf { !it.isNullOrEmpty() } + ?.let { statuses -> listOf(EnumFilter.In(statuses)) } ?: emptyList(), ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt index b7b6c4e2ec..58b87abddd 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt @@ -51,7 +51,9 @@ internal abstract class BaseDateEvaluator( return item.filters.joinToString(" OR ") { "(${getFilterWhereClause(it)})" } } } + private fun getFilterWhereClause(filter: DateFilter): String { + val filterHelper = FilterHelper(item.id) return when (filter) { is DateFilter.Absolute -> { val periodType = PeriodType.periodTypeFromPeriodId(filter.uid) @@ -70,6 +72,15 @@ internal abstract class BaseDateEvaluator( is DateFilter.Range -> { betweenDates(filter.startDate, filter.endDate) } + + is DateFilter.EqualTo -> filterHelper.equalTo(filter.timestamp) + is DateFilter.NotEqualTo -> filterHelper.notEqualTo(filter.timestamp) + is DateFilter.EqualToIgnoreCase -> filterHelper.notEqualTo(filter.timestamp) + is DateFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.timestamp) + is DateFilter.Like -> filterHelper.like(filter.timestamp) + is DateFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.timestamp) + is DateFilter.NotLike -> filterHelper.notLike(filter.timestamp) + is DateFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.timestamp) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt new file mode 100644 index 0000000000..d30e244048 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +import org.hisp.dhis.android.core.analytics.trackerlinelist.EnumFilter + +internal abstract class BaseEnumEvaluator>( + private val itemId: String, + private val filters: List>, +) : TrackerLineListEvaluator() { + + fun getWhereClause(): String { + return if (filters.isEmpty()) { + "1" + } else { + return filters.joinToString(" OR ") { "(${getFilterWhereClause(it)})" } + } + } + + private fun getFilterWhereClause(filter: EnumFilter): String { + val filterHelper = FilterHelper(itemId) + return when (filter) { + is EnumFilter.EqualTo -> filterHelper.equalTo(filter.value) + is EnumFilter.NotEqualTo -> filterHelper.notEqualTo(filter.value) + is EnumFilter.EqualToIgnoreCase -> filterHelper.notEqualTo(filter.value) + is EnumFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.value) + is EnumFilter.Like -> filterHelper.like(filter.value) + is EnumFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.value) + is EnumFilter.NotLike -> filterHelper.notLike(filter.value) + is EnumFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.value) + is EnumFilter.In -> filterHelper.inValues(filter.values.map { it.name }) + } + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt similarity index 62% rename from core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt rename to core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt index 4637b2155f..fd19925534 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/DataFilterHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.android.core.analytics.trackerlinelist.internal +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter @@ -35,27 +35,26 @@ internal object DataFilterHelper { return if (filters.isEmpty()) { "1" } else { - return filters.joinToString(" AND ") { getSqlOperator(itemId, it) } + val filterHelper = FilterHelper(itemId) + return filters.joinToString(" AND ") { getSqlOperator(filterHelper, it) } } } - private fun getSqlOperator(itemId: String, filter: DataFilter): String { - val comparison = when (filter) { - is DataFilter.EqualTo -> "= '${filter.value}'" - is DataFilter.NotEqualTo -> "!= '${filter.value}'" - is DataFilter.EqualToIgnoreCase -> "= '${filter.value}' COLLATE NOCASE" - is DataFilter.NotEqualToIgnoreCase -> "!= '${filter.value}' COLLATE NOCASE" - is DataFilter.GreaterThan -> "> ${filter.value}" - is DataFilter.GreaterThanOrEqualTo -> ">= ${filter.value}" - is DataFilter.LowerThan -> "< ${filter.value}" - is DataFilter.LowerThanOrEqualTo -> "<= ${filter.value}" - is DataFilter.Like -> "= '%${filter.value}%'" - is DataFilter.LikeIgnoreCase -> "= '%${filter.value}%' COLLATE NOCASE" - is DataFilter.NotLike -> "!= '%${filter.value}%'" - is DataFilter.NotLikeIgnoreCase -> "!= '%${filter.value}%' COLLATE NOCASE" - is DataFilter.In -> "IN (${filter.values.joinToString(", ") {"'$it'"}})" + private fun getSqlOperator(helper: FilterHelper, filter: DataFilter): String { + return when (filter) { + is DataFilter.EqualTo -> helper.equalTo(filter.value) + is DataFilter.NotEqualTo -> helper.notEqualTo(filter.value) + is DataFilter.EqualToIgnoreCase -> helper.equalToIgnoreCase(filter.value) + is DataFilter.NotEqualToIgnoreCase -> helper.notEqualToIgnoreCase(filter.value) + is DataFilter.GreaterThan -> helper.greaterThan(filter.value) + is DataFilter.GreaterThanOrEqualTo -> helper.greaterThanOrEqualTo(filter.value) + is DataFilter.LowerThan -> helper.lowerThan(filter.value) + is DataFilter.LowerThanOrEqualTo -> helper.lowerThanOrEqualTo(filter.value) + is DataFilter.Like -> helper.like(filter.value) + is DataFilter.LikeIgnoreCase -> helper.likeIgnoreCase(filter.value) + is DataFilter.NotLike -> helper.notLike(filter.value) + is DataFilter.NotLikeIgnoreCase -> helper.notLikeIgnoreCase(filter.value) + is DataFilter.In -> helper.inValues(filter.values) } - - return "\"$itemId\" $comparison" } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt index 6a60b3b5d0..84a83112fb 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/EventStatusEvaluator.kt @@ -31,11 +31,12 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias +import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.event.EventTableInfo internal class EventStatusEvaluator( - private val item: TrackerLineListItem.EventStatusItem, -) : TrackerLineListEvaluator() { + item: TrackerLineListItem.EventStatusItem, +) : BaseEnumEvaluator(item.id, item.filters) { override fun getSelectSQLForEvent(): String { return "$EventAlias.${EventTableInfo.Columns.STATUS}" @@ -46,11 +47,7 @@ internal class EventStatusEvaluator( } override fun getWhereSQLForEvent(): String { - return if (item.filters.isEmpty()) { - "1" - } else { - "${item.id} IN (${item.filters.joinToString(", ") { "'${it.name}'" }})" - } + return getWhereClause() } override fun getWhereSQLForEnrollment(): String { diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt new file mode 100644 index 0000000000..4671c88c35 --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator + +@Suppress("TooManyFunctions") +internal class FilterHelper(private val itemId: String) { + fun equalTo(value: String): String = itemTo("= '$value'") + fun notEqualTo(value: String): String = itemTo("!= '$value'") + fun equalToIgnoreCase(value: String): String = itemTo("= '$value' COLLATE NOCASE") + fun notEqualToIgnoreCase(value: String): String = itemTo("!= '$value' COLLATE NOCASE") + fun greaterThan(value: String): String = itemTo("> $value") + fun greaterThanOrEqualTo(value: String): String = itemTo(">= $value") + fun lowerThan(value: String): String = itemTo("< $value") + fun lowerThanOrEqualTo(value: String): String = itemTo("<= $value") + fun like(value: String): String = itemTo("LIKE '%$value%'") + fun likeIgnoreCase(value: String): String = itemTo("LIKE '%$value%' COLLATE NOCASE") + fun notLike(value: String): String = itemTo("NOT LIKE '%$value%'") + fun notLikeIgnoreCase(value: String): String = itemTo("NOT LIKE '%$value%' COLLATE NOCASE") + fun inValues(values: List): String = itemTo("IN (${values.joinToString(", ") { "'$it'" }})") + + private fun itemTo(comparison: String) = "\"$itemId\" $comparison" +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt index 83df77348a..481f43755c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt @@ -64,7 +64,9 @@ internal class OrganisationUnitEvaluator( } } + @Suppress("ComplexMethod") private fun getFilterWhereClause(filter: OrganisationUnitFilter): String { + val filterHelper = FilterHelper(item.id) return when (filter) { is OrganisationUnitFilter.Absolute -> inPathOf(filter.uid) @@ -115,6 +117,15 @@ internal class OrganisationUnitEvaluator( inPathOfAny(orgunits.mapNotNull { it.organisationUnit() }) } + + is OrganisationUnitFilter.EqualTo -> filterHelper.equalTo(filter.orgunitName) + is OrganisationUnitFilter.NotEqualTo -> filterHelper.notEqualTo(filter.orgunitName) + is OrganisationUnitFilter.EqualToIgnoreCase -> filterHelper.equalToIgnoreCase(filter.orgunitName) + is OrganisationUnitFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.orgunitName) + is OrganisationUnitFilter.Like -> filterHelper.like(filter.orgunitName) + is OrganisationUnitFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.orgunitName) + is OrganisationUnitFilter.NotLike -> filterHelper.notLike(filter.orgunitName) + is OrganisationUnitFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.orgunitName) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt index 2af9141663..fc18b5c48f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramAttributeEvaluator.kt @@ -31,7 +31,6 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem -import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt index 0d22a8eef6..c3e925461f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramDataElementEvaluator.kt @@ -31,7 +31,6 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem -import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EventAlias import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt index 4941b0be97..85f4e9c70c 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramIndicatorEvaluator.kt @@ -31,7 +31,6 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.analytics.aggregated.MetadataItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem -import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.DataFilterHelper import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.TrackerLineListContext import org.hisp.dhis.android.core.arch.helpers.UidsHelper import org.hisp.dhis.android.core.constant.Constant diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt index c43a99e86a..7b14c1a8a8 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/ProgramStatusEvaluator.kt @@ -30,21 +30,18 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.EnrollmentAlias +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo internal class ProgramStatusEvaluator( private val item: TrackerLineListItem.ProgramStatusItem, -) : TrackerLineListEvaluator() { +) : BaseEnumEvaluator(item.id, item.filters) { override fun getCommonSelectSQL(): String { return "$EnrollmentAlias.${EnrollmentTableInfo.Columns.STATUS}" } override fun getCommonWhereSQL(): String { - return if (item.filters.isEmpty()) { - "1" - } else { - "${item.id} IN (${item.filters.joinToString(", ") { "'${it.name}'" }})" - } + return getWhereClause() } } From 2d1ff04e83165577f84dddcf804fe09a143446d6 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 7 Mar 2024 11:49:06 +0100 Subject: [PATCH 187/222] [ANDROSDK-1830] Adapt unit tests --- .../internal/TrackerVisualizationMapperShould.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt index 19b773ba3f..b0e8950145 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt @@ -31,6 +31,7 @@ import com.google.common.truth.Truth.assertThat import com.nhaarman.mockitokotlin2.mock import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.DateFilter +import org.hisp.dhis.android.core.analytics.trackerlinelist.EnumFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -89,12 +90,24 @@ class TrackerVisualizationMapperShould { .items( listOf( ObjectWithUid.create(EnrollmentStatus.ACTIVE.name), + ObjectWithUid.create(EnrollmentStatus.CANCELLED.name), ), ) .build() val programStatus = mapper.mapDataX(item) - assertThat(programStatus).isEqualTo(TrackerLineListItem.ProgramStatusItem(listOf(EnrollmentStatus.ACTIVE))) + assertThat(programStatus).isEqualTo( + TrackerLineListItem.ProgramStatusItem( + listOf( + EnumFilter.In( + listOf( + EnrollmentStatus.ACTIVE, + EnrollmentStatus.CANCELLED, + ) + ) + ) + ) + ) } } From d278d628bca423531be1b682473c294e24b3e4a0 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 7 Mar 2024 12:21:02 +0100 Subject: [PATCH 188/222] [ANDROSDK-1830] Set ignoreCase as a method parameter --- ...rackerLineListRepositoryEvaluatorShould.kt | 2 +- .../trackerlinelist/TrackerLineListModel.kt | 48 +++++++------------ .../internal/TrackerVisualizationMapper.kt | 16 +++---- .../internal/evaluator/BaseDateEvaluator.kt | 12 ++--- .../internal/evaluator/BaseEnumEvaluator.kt | 12 ++--- .../internal/evaluator/DataFilterHelper.kt | 12 ++--- .../internal/evaluator/FilterHelper.kt | 14 +++--- .../evaluator/OrganisationUnitEvaluator.kt | 13 ++--- .../TrackerVisualizationMapperShould.kt | 12 ++--- 9 files changed, 53 insertions(+), 88 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 003852f2ea..649ff432ed 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -259,7 +259,7 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati helper.createSingleEvent(event2, program.uid(), programStage1.uid(), orgunitChild2.uid()) val result = d2.analyticsModule().trackerLineList() - .withEventOutput(program.uid(), programStage1.uid()) + .withEventOutput(programStage1.uid()) .withColumn( TrackerLineListItem.OrganisationUnitItem( filters = listOf( diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index c663f9c5f1..0a2268e044 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -94,55 +94,39 @@ sealed class OrganisationUnitFilter { data class Relative(val relative: RelativeOrganisationUnit) : OrganisationUnitFilter() data class Level(val uid: String) : OrganisationUnitFilter() data class Group(val uid: String) : OrganisationUnitFilter() - data class EqualTo(val orgunitName: String) : OrganisationUnitFilter() - data class NotEqualTo(val orgunitName: String) : OrganisationUnitFilter() - data class EqualToIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() - data class NotEqualToIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() - data class Like(val orgunitName: String) : OrganisationUnitFilter() - data class NotLike(val orgunitName: String) : OrganisationUnitFilter() - data class LikeIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() - data class NotLikeIgnoreCase(val orgunitName: String) : OrganisationUnitFilter() + data class EqualTo(val orgunitName: String, val ignoreCase: Boolean = false) : OrganisationUnitFilter() + data class NotEqualTo(val orgunitName: String, val ignoreCase: Boolean = false) : OrganisationUnitFilter() + data class Like(val orgunitName: String, val ignoreCase: Boolean = true) : OrganisationUnitFilter() + data class NotLike(val orgunitName: String, val ignoreCase: Boolean = true) : OrganisationUnitFilter() } sealed class DateFilter { data class Relative(val relative: RelativePeriod) : DateFilter() data class Absolute(val uid: String) : DateFilter() data class Range(val startDate: String, val endDate: String) : DateFilter() - data class EqualTo(val timestamp: String) : DateFilter() - data class NotEqualTo(val timestamp: String) : DateFilter() - data class EqualToIgnoreCase(val timestamp: String) : DateFilter() - data class NotEqualToIgnoreCase(val timestamp: String) : DateFilter() - data class Like(val timestamp: String) : DateFilter() - data class NotLike(val timestamp: String) : DateFilter() - data class LikeIgnoreCase(val timestamp: String) : DateFilter() - data class NotLikeIgnoreCase(val timestamp: String) : DateFilter() + data class EqualTo(val timestamp: String, val ignoreCase: Boolean = false) : DateFilter() + data class NotEqualTo(val timestamp: String, val ignoreCase: Boolean = false) : DateFilter() + data class Like(val timestamp: String, val ignoreCase: Boolean = true) : DateFilter() + data class NotLike(val timestamp: String, val ignoreCase: Boolean = true) : DateFilter() } sealed class EnumFilter { - data class EqualTo(val value: String) : EnumFilter() - data class NotEqualTo(val value: String) : EnumFilter() - data class EqualToIgnoreCase(val value: String) : EnumFilter() - data class NotEqualToIgnoreCase(val value: String) : EnumFilter() - data class Like(val value: String) : EnumFilter() - data class NotLike(val value: String) : EnumFilter() - data class LikeIgnoreCase(val value: String) : EnumFilter() - data class NotLikeIgnoreCase(val value: String) : EnumFilter() + data class EqualTo(val value: String, val ignoreCase: Boolean = false) : EnumFilter() + data class NotEqualTo(val value: String, val ignoreCase: Boolean = false) : EnumFilter() + data class Like(val value: String, val ignoreCase: Boolean = true) : EnumFilter() + data class NotLike(val value: String, val ignoreCase: Boolean = true) : EnumFilter() data class In(val values: List) : EnumFilter() } sealed class DataFilter { - data class EqualTo(val value: String) : DataFilter() - data class NotEqualTo(val value: String) : DataFilter() - data class EqualToIgnoreCase(val value: String) : DataFilter() - data class NotEqualToIgnoreCase(val value: String) : DataFilter() + data class EqualTo(val value: String, val ignoreCase: Boolean = false) : DataFilter() + data class NotEqualTo(val value: String, val ignoreCase: Boolean = false) : DataFilter() data class GreaterThan(val value: String) : DataFilter() data class GreaterThanOrEqualTo(val value: String) : DataFilter() data class LowerThan(val value: String) : DataFilter() data class LowerThanOrEqualTo(val value: String) : DataFilter() - data class Like(val value: String) : DataFilter() - data class NotLike(val value: String) : DataFilter() - data class LikeIgnoreCase(val value: String) : DataFilter() - data class NotLikeIgnoreCase(val value: String) : DataFilter() + data class Like(val value: String, val ignoreCase: Boolean = true) : DataFilter() + data class NotLike(val value: String, val ignoreCase: Boolean = true) : DataFilter() data class In(val values: List) : DataFilter() } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt index 591be13ba0..42512917b5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapper.kt @@ -200,19 +200,19 @@ internal class TrackerVisualizationMapper( val value = filterPair.getOrNull(1) if (operator != null && value != null) { when (operator) { - "EQ" -> DataFilter.EqualTo(value) - "!EQ" -> DataFilter.NotEqualTo(value) - "IEQ" -> DataFilter.EqualToIgnoreCase(value) - "!IEQ" -> DataFilter.NotEqualToIgnoreCase(value) + "EQ" -> DataFilter.EqualTo(value, ignoreCase = false) + "!EQ" -> DataFilter.NotEqualTo(value, ignoreCase = false) + "IEQ" -> DataFilter.EqualTo(value, ignoreCase = true) + "!IEQ" -> DataFilter.NotEqualTo(value, ignoreCase = true) "GT" -> DataFilter.GreaterThan(value) "GE" -> DataFilter.GreaterThanOrEqualTo(value) "LT" -> DataFilter.LowerThan(value) "LE" -> DataFilter.LowerThanOrEqualTo(value) "NE" -> DataFilter.NotEqualTo(value) - "LIKE" -> DataFilter.Like(value) - "!LIKE" -> DataFilter.NotLike(value) - "ILIKE" -> DataFilter.LikeIgnoreCase(value) - "!ILIKE" -> DataFilter.NotLikeIgnoreCase(value) + "LIKE" -> DataFilter.Like(value, ignoreCase = false) + "!LIKE" -> DataFilter.NotLike(value, ignoreCase = false) + "ILIKE" -> DataFilter.Like(value, ignoreCase = true) + "!ILIKE" -> DataFilter.NotLike(value, ignoreCase = true) "IN" -> DataFilter.In(value.split(";")) else -> null } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt index 58b87abddd..8b5af2e665 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseDateEvaluator.kt @@ -73,14 +73,10 @@ internal abstract class BaseDateEvaluator( betweenDates(filter.startDate, filter.endDate) } - is DateFilter.EqualTo -> filterHelper.equalTo(filter.timestamp) - is DateFilter.NotEqualTo -> filterHelper.notEqualTo(filter.timestamp) - is DateFilter.EqualToIgnoreCase -> filterHelper.notEqualTo(filter.timestamp) - is DateFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.timestamp) - is DateFilter.Like -> filterHelper.like(filter.timestamp) - is DateFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.timestamp) - is DateFilter.NotLike -> filterHelper.notLike(filter.timestamp) - is DateFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.timestamp) + is DateFilter.EqualTo -> filterHelper.equalTo(filter.timestamp, filter.ignoreCase) + is DateFilter.NotEqualTo -> filterHelper.notEqualTo(filter.timestamp, filter.ignoreCase) + is DateFilter.Like -> filterHelper.like(filter.timestamp, filter.ignoreCase) + is DateFilter.NotLike -> filterHelper.notLike(filter.timestamp, filter.ignoreCase) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt index d30e244048..0f0229618e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/BaseEnumEvaluator.kt @@ -46,14 +46,10 @@ internal abstract class BaseEnumEvaluator>( private fun getFilterWhereClause(filter: EnumFilter): String { val filterHelper = FilterHelper(itemId) return when (filter) { - is EnumFilter.EqualTo -> filterHelper.equalTo(filter.value) - is EnumFilter.NotEqualTo -> filterHelper.notEqualTo(filter.value) - is EnumFilter.EqualToIgnoreCase -> filterHelper.notEqualTo(filter.value) - is EnumFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.value) - is EnumFilter.Like -> filterHelper.like(filter.value) - is EnumFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.value) - is EnumFilter.NotLike -> filterHelper.notLike(filter.value) - is EnumFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.value) + is EnumFilter.EqualTo -> filterHelper.equalTo(filter.value, filter.ignoreCase) + is EnumFilter.NotEqualTo -> filterHelper.notEqualTo(filter.value, filter.ignoreCase) + is EnumFilter.Like -> filterHelper.like(filter.value, filter.ignoreCase) + is EnumFilter.NotLike -> filterHelper.notLike(filter.value, filter.ignoreCase) is EnumFilter.In -> filterHelper.inValues(filter.values.map { it.name }) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt index fd19925534..8ba62a6b09 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/DataFilterHelper.kt @@ -42,18 +42,14 @@ internal object DataFilterHelper { private fun getSqlOperator(helper: FilterHelper, filter: DataFilter): String { return when (filter) { - is DataFilter.EqualTo -> helper.equalTo(filter.value) - is DataFilter.NotEqualTo -> helper.notEqualTo(filter.value) - is DataFilter.EqualToIgnoreCase -> helper.equalToIgnoreCase(filter.value) - is DataFilter.NotEqualToIgnoreCase -> helper.notEqualToIgnoreCase(filter.value) + is DataFilter.EqualTo -> helper.equalTo(filter.value, filter.ignoreCase) + is DataFilter.NotEqualTo -> helper.notEqualTo(filter.value, filter.ignoreCase) is DataFilter.GreaterThan -> helper.greaterThan(filter.value) is DataFilter.GreaterThanOrEqualTo -> helper.greaterThanOrEqualTo(filter.value) is DataFilter.LowerThan -> helper.lowerThan(filter.value) is DataFilter.LowerThanOrEqualTo -> helper.lowerThanOrEqualTo(filter.value) - is DataFilter.Like -> helper.like(filter.value) - is DataFilter.LikeIgnoreCase -> helper.likeIgnoreCase(filter.value) - is DataFilter.NotLike -> helper.notLike(filter.value) - is DataFilter.NotLikeIgnoreCase -> helper.notLikeIgnoreCase(filter.value) + is DataFilter.Like -> helper.like(filter.value, filter.ignoreCase) + is DataFilter.NotLike -> helper.notLike(filter.value, filter.ignoreCase) is DataFilter.In -> helper.inValues(filter.values) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt index 4671c88c35..6c9cfaa923 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/FilterHelper.kt @@ -30,19 +30,17 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator @Suppress("TooManyFunctions") internal class FilterHelper(private val itemId: String) { - fun equalTo(value: String): String = itemTo("= '$value'") - fun notEqualTo(value: String): String = itemTo("!= '$value'") - fun equalToIgnoreCase(value: String): String = itemTo("= '$value' COLLATE NOCASE") - fun notEqualToIgnoreCase(value: String): String = itemTo("!= '$value' COLLATE NOCASE") + fun equalTo(value: String, ignoreCase: Boolean): String = itemTo("= '$value'${case(ignoreCase)}") + fun notEqualTo(value: String, ignoreCase: Boolean): String = itemTo("!= '$value'${case(ignoreCase)}") fun greaterThan(value: String): String = itemTo("> $value") fun greaterThanOrEqualTo(value: String): String = itemTo(">= $value") fun lowerThan(value: String): String = itemTo("< $value") fun lowerThanOrEqualTo(value: String): String = itemTo("<= $value") - fun like(value: String): String = itemTo("LIKE '%$value%'") - fun likeIgnoreCase(value: String): String = itemTo("LIKE '%$value%' COLLATE NOCASE") - fun notLike(value: String): String = itemTo("NOT LIKE '%$value%'") - fun notLikeIgnoreCase(value: String): String = itemTo("NOT LIKE '%$value%' COLLATE NOCASE") + fun like(value: String, ignoreCase: Boolean): String = itemTo("LIKE '%$value%'${case(ignoreCase)}") + fun notLike(value: String, ignoreCase: Boolean): String = itemTo("NOT LIKE '%$value%'${case(ignoreCase)}") fun inValues(values: List): String = itemTo("IN (${values.joinToString(", ") { "'$it'" }})") private fun itemTo(comparison: String) = "\"$itemId\" $comparison" + + private fun case(ignoreCase: Boolean) = if (ignoreCase) " COLLATE NOCASE" else "" } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt index 481f43755c..eb2794f713 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/OrganisationUnitEvaluator.kt @@ -64,7 +64,6 @@ internal class OrganisationUnitEvaluator( } } - @Suppress("ComplexMethod") private fun getFilterWhereClause(filter: OrganisationUnitFilter): String { val filterHelper = FilterHelper(item.id) return when (filter) { @@ -118,14 +117,10 @@ internal class OrganisationUnitEvaluator( inPathOfAny(orgunits.mapNotNull { it.organisationUnit() }) } - is OrganisationUnitFilter.EqualTo -> filterHelper.equalTo(filter.orgunitName) - is OrganisationUnitFilter.NotEqualTo -> filterHelper.notEqualTo(filter.orgunitName) - is OrganisationUnitFilter.EqualToIgnoreCase -> filterHelper.equalToIgnoreCase(filter.orgunitName) - is OrganisationUnitFilter.NotEqualToIgnoreCase -> filterHelper.notEqualToIgnoreCase(filter.orgunitName) - is OrganisationUnitFilter.Like -> filterHelper.like(filter.orgunitName) - is OrganisationUnitFilter.LikeIgnoreCase -> filterHelper.likeIgnoreCase(filter.orgunitName) - is OrganisationUnitFilter.NotLike -> filterHelper.notLike(filter.orgunitName) - is OrganisationUnitFilter.NotLikeIgnoreCase -> filterHelper.notLikeIgnoreCase(filter.orgunitName) + is OrganisationUnitFilter.EqualTo -> filterHelper.equalTo(filter.orgunitName, filter.ignoreCase) + is OrganisationUnitFilter.NotEqualTo -> filterHelper.notEqualTo(filter.orgunitName, filter.ignoreCase) + is OrganisationUnitFilter.Like -> filterHelper.like(filter.orgunitName, filter.ignoreCase) + is OrganisationUnitFilter.NotLike -> filterHelper.notLike(filter.orgunitName, filter.ignoreCase) } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt index b0e8950145..23eddebc25 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerVisualizationMapperShould.kt @@ -58,8 +58,8 @@ class TrackerVisualizationMapperShould { assertThat(dataFilters).containsExactly( DataFilter.GreaterThan("6"), - DataFilter.LikeIgnoreCase("ar"), - DataFilter.NotEqualTo("4"), + DataFilter.Like("ar", ignoreCase = true), + DataFilter.NotEqualTo("4", ignoreCase = false), ) } @@ -104,10 +104,10 @@ class TrackerVisualizationMapperShould { listOf( EnrollmentStatus.ACTIVE, EnrollmentStatus.CANCELLED, - ) - ) - ) - ) + ), + ), + ), + ), ) } } From 4fab3d5e74436f18d4a52dfe242e97554177084b Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Thu, 7 Mar 2024 12:37:08 +0100 Subject: [PATCH 189/222] [ANDROSDK-1833] Add method to retrieve current account --- .../AccountManagerMockIntegrationShould.kt | 25 +++++++++++++++++++ .../dhis/android/core/user/AccountManager.kt | 2 ++ .../core/user/internal/AccountManagerImpl.kt | 8 +++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/testapp/user/AccountManagerMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/testapp/user/AccountManagerMockIntegrationShould.kt index 1ce17450f0..94753ff0f2 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/testapp/user/AccountManagerMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/testapp/user/AccountManagerMockIntegrationShould.kt @@ -69,6 +69,31 @@ class AccountManagerMockIntegrationShould : BaseMockIntegrationTestEmptyEnqueabl loginAndDeleteAccount(user1, pass1, dhis2MockServer) } + @Test + fun find_current_account_after_login() { + if (d2.userModule().blockingIsLogged()) { + d2.userModule().blockingLogOut() + } + dhis2MockServer.enqueueLoginResponses() + d2.userModule().blockingLogIn(user1, pass1, dhis2MockServer.baseEndpoint) + + val currentAccount = d2.userModule().accountManager().getCurrentAccount() + assertThat(currentAccount?.username()).isEqualTo(user1) + assertThat(currentAccount?.syncState()).isNotNull() + + loginAndDeleteAccount(user1, pass1, dhis2MockServer) + } + + @Test + fun cannot_find_current_account_after_logout() { + if (d2.userModule().blockingIsLogged()) { + d2.userModule().blockingLogOut() + } + + val currentAccount = d2.userModule().accountManager().getCurrentAccount() + assertThat(currentAccount).isNull() + } + @Test fun can_change_max_accounts() { d2.userModule().accountManager().setMaxAccounts(5) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/AccountManager.kt b/core/src/main/java/org/hisp/dhis/android/core/user/AccountManager.kt index c9d465a812..498a17bef7 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/AccountManager.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/AccountManager.kt @@ -36,6 +36,8 @@ import kotlin.jvm.Throws interface AccountManager { fun getAccounts(): List + fun getCurrentAccount(): DatabaseAccount? + fun setMaxAccounts(maxAccounts: Int?) fun getMaxAccounts(): Int? diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt index 2933c0565f..3397d4d09f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/AccountManagerImpl.kt @@ -49,7 +49,7 @@ import org.koin.core.annotation.Singleton @Singleton @Suppress("TooManyFunctions") -internal class AccountManagerImpl constructor( +internal class AccountManagerImpl( private val databasesConfigurationStore: DatabaseConfigurationInsecureStore, private val multiUserDatabaseManager: MultiUserDatabaseManager, private val databaseAdapterFactory: DatabaseAdapterFactory, @@ -63,6 +63,12 @@ internal class AccountManagerImpl constructor( return databasesConfigurationStore.get()?.accounts()?.map { updateSyncState(it) } ?: emptyList() } + override fun getCurrentAccount(): DatabaseAccount? { + return credentialsSecureStore.get() + ?.let { multiUserDatabaseManager.getAccount(it.serverUrl, it.username) } + ?.let { updateSyncState(it) } + } + override fun setMaxAccounts(maxAccounts: Int?) { multiUserDatabaseManager.setMaxAccounts(maxAccounts) } From d9c96a40f501a1b5679c797d0cd855d0a7dad5e8 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 8 Mar 2024 10:00:37 +0100 Subject: [PATCH 190/222] [ANDROSDK-1829] Add paging to TrackerLineList repository --- ...rackerLineListRepositoryEvaluatorShould.kt | 36 +++++++++++++++++++ .../TrackerLineListRepository.kt | 3 ++ .../internal/TrackerLineListParams.kt | 6 ++++ .../internal/TrackerLineListRepositoryImpl.kt | 5 +++ .../internal/TrackerLineListService.kt | 14 ++++++-- .../arch/repositories/paging/PageConfig.kt | 34 ++++++++++++++++++ 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/PageConfig.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index 649ff432ed..a9be59f6fe 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -45,6 +45,7 @@ import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEv import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntity2 import org.hisp.dhis.android.core.analytics.aggregated.internal.evaluator.BaseEvaluatorSamples.trackedEntityType import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.android.core.arch.repositories.paging.PageConfig import org.hisp.dhis.android.core.common.AnalyticsType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.event.EventStatus @@ -294,6 +295,41 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati assertThat(rows.flatten().map { it.value }).containsExactly("5", "10") } + @Test + fun evaluate_paging() { + val event1 = generator.generate() + helper.createSingleEvent(event1, program.uid(), programStage1.uid(), orgunitChild1.uid()) + val event2 = generator.generate() + helper.createSingleEvent(event2, program.uid(), programStage1.uid(), orgunitChild2.uid()) + + val baseRepo = d2.analyticsModule().trackerLineList() + .withEventOutput(programStage1.uid()) + .withColumn(TrackerLineListItem.OrganisationUnitItem()) + + // Page 1 + val resultPage1 = baseRepo + .withPageConfig(PageConfig.Paging(1, 1)) + .blockingEvaluate() + + assertThat(resultPage1.getOrThrow().rows.size).isEqualTo(1) + assertThat(resultPage1.getOrThrow().rows.first().first().value).isEqualTo(orgunitChild1.displayName()) + + // Page 2 + val resultPage2 = baseRepo + .withPageConfig(PageConfig.Paging(2, 1)) + .blockingEvaluate() + + assertThat(resultPage2.getOrThrow().rows.size).isEqualTo(1) + assertThat(resultPage2.getOrThrow().rows.first().first().value).isEqualTo(orgunitChild2.displayName()) + + // No paging + val resultNoPaging = baseRepo + .withPageConfig(PageConfig.NoPaging) + .blockingEvaluate() + + assertThat(resultNoPaging.getOrThrow().rows.size).isEqualTo(2) + } + private fun createDefaultEnrollment( teiUid: String, enrollmentUid: String, diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt index 88467d8f87..a5fae75944 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepository.kt @@ -31,6 +31,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist import io.reactivex.Single import org.hisp.dhis.android.core.analytics.AnalyticsException import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.arch.repositories.paging.PageConfig interface TrackerLineListRepository { @@ -44,6 +45,8 @@ interface TrackerLineListRepository { fun withTrackerVisualization(trackerVisualization: String): TrackerLineListRepository + fun withPageConfig(pageConfig: PageConfig): TrackerLineListRepository + fun evaluate(): Single> fun blockingEvaluate(): Result diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt index 76eeb19c10..f0ca6a8da9 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListParams.kt @@ -29,6 +29,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem +import org.hisp.dhis.android.core.arch.repositories.paging.PageConfig import org.hisp.dhis.android.core.util.replaceOrPush internal data class TrackerLineListParams( @@ -38,6 +39,7 @@ internal data class TrackerLineListParams( val programStageId: String?, val columns: List, val filters: List, + val pageConfig: PageConfig = DefaultPaging, ) { val allItems = columns + filters @@ -99,4 +101,8 @@ internal data class TrackerLineListParams( val (positive, negativeOrZero) = indexes.sorted().partition { it > 0 } return positive + negativeOrZero } + + companion object { + val DefaultPaging = PageConfig.Paging(1, 500) + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt index 292c219cd1..f6197ecdc4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListRepositoryImpl.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListRepository import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListResponse import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.arch.repositories.paging.PageConfig import org.koin.core.annotation.Singleton @Singleton @@ -72,6 +73,10 @@ internal class TrackerLineListRepositoryImpl( return updateParams { params.copy(trackerVisualization = trackerVisualization) } } + override fun withPageConfig(pageConfig: PageConfig): TrackerLineListRepository { + return updateParams { params.copy(pageConfig = pageConfig) } + } + override fun evaluate(): Single> { return Single.fromCallable { blockingEvaluate() } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt index c25cffb12f..d81993e1ed 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/TrackerLineListService.kt @@ -38,6 +38,7 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.T import org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator.TrackerLineListSQLLabel.OrgunitAlias import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.arch.repositories.paging.PageConfig import org.hisp.dhis.android.core.enrollment.EnrollmentTableInfo import org.hisp.dhis.android.core.event.EventTableInfo import org.hisp.dhis.android.core.organisationunit.OrganisationUnitTableInfo @@ -127,7 +128,8 @@ internal class TrackerLineListService( } + "WHERE " + "$EventAlias.${EventTableInfo.Columns.PROGRAM_STAGE} = '${params.programStageId!!}' AND " + - "${getEventWhereClause(params, context)} " + "${getEventWhereClause(params, context)} " + + appendPaging(params.pageConfig) } private fun getEnrollmentSqlClause(params: TrackerLineListParams, context: TrackerLineListContext): String { @@ -143,7 +145,8 @@ internal class TrackerLineListService( } + "WHERE " + "$EnrollmentAlias.${EnrollmentTableInfo.Columns.PROGRAM} = '${params.programId!!}' AND " + - "${getEnrollmentWhereClause(params, context)} " + "${getEnrollmentWhereClause(params, context)} " + + appendPaging(params.pageConfig) } private fun getEventSelectColumns(params: TrackerLineListParams, context: TrackerLineListContext): String { @@ -179,4 +182,11 @@ internal class TrackerLineListService( "($orClause)" } } + + private fun appendPaging(pageConfig: PageConfig): String { + return when (pageConfig) { + is PageConfig.NoPaging -> "" + is PageConfig.Paging -> "LIMIT ${pageConfig.pageSize} OFFSET ${pageConfig.pageSize * (pageConfig.page - 1)}" + } + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/PageConfig.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/PageConfig.kt new file mode 100644 index 0000000000..d3c819a24b --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/repositories/paging/PageConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.hisp.dhis.android.core.arch.repositories.paging + +sealed class PageConfig { + data object NoPaging : PageConfig() + data class Paging(val page: Int, val pageSize: Int) : PageConfig() +} From 03541fe074ea629b2ea93a59370026bc61e762b1 Mon Sep 17 00:00:00 2001 From: Marcos Campos Date: Mon, 11 Mar 2024 11:10:30 +0100 Subject: [PATCH 191/222] [ANDROSDK-1835] Fix crash on event expired method when event and due dates are null --- .../core/event/internal/EventDateUtils.kt | 23 ++++++++++--------- .../event/internal/EventDateUtilsShould.kt | 9 ++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt index 1a8515dc12..a1976d7733 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventDateUtils.kt @@ -81,19 +81,20 @@ class EventDateUtils( false } - val eventDateOrDueDate = event.eventDate() ?: event.dueDate() + val expiredBecauseOfPeriod = (event.eventDate() ?: event.dueDate())?.let { eventDateOrDueDate -> + programPeriodType?.let { periodType -> - val expiredBecauseOfPeriod = programPeriodType?.let { periodType -> - var nextPeriod = periodHelper - .blockingGetPeriodForPeriodTypeAndDate(periodType, eventDateOrDueDate!!, 1).startDate()!! - val currentDate: Date = getCalendar().time - if (expiryDays > 0) { - val calendar: Calendar = getCalendar() - calendar.time = nextPeriod - calendar.add(Calendar.DAY_OF_YEAR, expiryDays) - nextPeriod = calendar.time + var nextPeriod = periodHelper + .blockingGetPeriodForPeriodTypeAndDate(periodType, eventDateOrDueDate, 1).startDate()!! + val currentDate: Date = getCalendar().time + if (expiryDays > 0) { + val calendar: Calendar = getCalendar() + calendar.time = nextPeriod + calendar.add(Calendar.DAY_OF_YEAR, expiryDays) + nextPeriod = calendar.time + } + nextPeriod <= currentDate } - nextPeriod <= currentDate } ?: false return expiredBecauseOfCompletion || expiredBecauseOfPeriod diff --git a/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventDateUtilsShould.kt b/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventDateUtilsShould.kt index 00e57e0a78..e0eee74870 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventDateUtilsShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventDateUtilsShould.kt @@ -96,6 +96,15 @@ class EventDateUtilsShould { assertThat(eventDateUtils.isEventExpired(event, 0, null, 2)).isFalse() } + @Test + fun `Should return is not expired if no event or due date provided`() { + whenever(event.status()) doReturn EventStatus.ACTIVE + whenever(event.eventDate()) doReturn null + whenever(event.dueDate()) doReturn null + + assertThat(eventDateUtils.isEventExpired(event, 0, null, 2)).isFalse() + } + private fun getCalendar(): Calendar { val calendar = Calendar.getInstance() calendar[Calendar.YEAR] = 2020 From c3a9661a6bc04c0df8fac0f10a4420803e8ef491 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 13 Mar 2024 08:45:21 +0100 Subject: [PATCH 192/222] [ANDROSDK-1836] Do not add ImageryProvider if no attribution --- .../externalmap/ExternalMapLayerCallFactory.kt | 14 ++++++++------ .../map/layer/externalmap/external_map_layers.json | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt index 2370734eba..4a51f6120b 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/map/layer/internal/externalmap/ExternalMapLayerCallFactory.kt @@ -67,12 +67,14 @@ internal class ExternalMapLayerCallFactory( .external(true) .imageUrl(externalMapLayer.url) .imageryProviders( - listOf( - MapLayerImageryProvider.builder() - .mapLayer(externalMapLayer.id) - .attribution(externalMapLayer.attribution) - .build(), - ), + externalMapLayer.attribution?.let { attribution -> + listOf( + MapLayerImageryProvider.builder() + .mapLayer(externalMapLayer.id) + .attribution(attribution) + .build(), + ) + } ?: emptyList(), ) .build() } diff --git a/core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json b/core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json index 7497958597..270df62d44 100644 --- a/core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json +++ b/core/src/sharedTest/resources/map/layer/externalmap/external_map_layers.json @@ -50,7 +50,6 @@ "lastUpdated": "2016-10-05T16:49:13.324", "mapService": "WMS", "url": "https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png", - "attribution": "Stamen Design, OpenStreetMap", "imageFormat": "PNG", "mapLayerPosition": "BASEMAP", "displayName": "Terrain basemap", From 8ee81359612a136135e93b73a921e244d943ddb3 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 13 Mar 2024 13:25:35 +0100 Subject: [PATCH 193/222] [ANDROSDK-1813] Import Zip4J dependencies --- core/build.gradle.kts | 2 ++ gradle/libs.versions.toml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 00cff339fd..8d09d4f8f8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -162,6 +162,8 @@ dependencies { // From SQLCipher 4.5.5, it depends on androidx.sqlite:sqlite api(libs.sqlite) + implementation(libs.zip4j) + implementation(libs.openid.appauth) implementation(libs.listenablefuture.empty) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecfc9a1f91..15169117c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ rxJava = "2.2.21" rxAndroid = "2.1.1" sqlCipher = "4.5.5" sqlLite = "2.2.0" +zip4j = "2.11.5" smsCompression = "0.2.0" expressionParser = "1.0.36" @@ -102,6 +103,8 @@ dhis2-antlr-parser = { group = "org.hisp.dhis.parser", name = "dhis-antlr-expres sqlcipher = { group = "net.zetetic", name = "sqlcipher-android", version.ref = "sqlCipher" } sqlite = { group = "androidx.sqlite", name = "sqlite", version.ref = "sqlLite" } +zip4j = { group = "net.lingala.zip4j", name = "zip4j", version.ref = "zip4j"} + openid-appauth = { group = "net.openid", name = "appauth", version.ref = "appauth" } listenablefuture-empty = { group = "com.google.guava", name = "listenablefuture", version.ref = "listenablefuture" } From b41944846adba21f11775a4d9277e85a2b006834 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 15 Mar 2024 12:56:20 +0100 Subject: [PATCH 194/222] [ANDROSDK-1813] Use encrypted zip to protect exported database --- .../assets/databases/export-database.zip | Bin 1016537 -> 1016255 bytes ...FromDatabaseAssetsMockIntegrationShould.kt | 3 + .../internal/DatabaseImportExportImpl.kt | 5 +- .../internal/MultiUserDatabaseManager.kt | 2 +- .../hisp/dhis/android/core/util/CipherUtil.kt | 78 +++++++----------- .../android/core/util/CipherUtilShould.kt | 41 --------- 6 files changed, 34 insertions(+), 95 deletions(-) delete mode 100644 core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt diff --git a/core/src/androidTest/assets/databases/export-database.zip b/core/src/androidTest/assets/databases/export-database.zip index fd3785920c9f28c538b179fb1097a2a71fc23168..eba424824343a6c0cfbeb17376208f40192fff8c 100644 GIT binary patch delta 619 zcmah{KWNlY9DaY2T#~!X9avDPL&c6ZQXD#n*O!DIltX(cg2h3uy$rUTceJq-L3+t4 z=qRX#wo?biQLpf%izL^fI5>!l9XdK%90UbLzbCHZ3;BK@-|x@&;P*b>ue|A1D)k!I zY))`i=O?aJ2WP6~Zi%RW1X{S-X)i@%%|_IiYb=LjouzgZ&PQR>ZO*xOZgyUN;-ddq z=%W9vYl*&*>Oz(&%?WwZqg=?*m_l(=Y72RRGE2ys$Dm({85MFiWvC1(htl#C-i|pc z@1?vbfrA^9ofTJcZB?sD*lric_E9wf$Z@_;i=4k z<5`x&6VZe*%oe30P?-en* zHBh+59rQj79OzGa4qP?|4(|Vgm!S89(n{qKd%i~5B>&gKM%5m1M!Gw(ivL@m{b$L9 zP;!Eh%Twb=7Y`_TOE=!F9MJCWk1ZAJHOhv_5HU?tJ(2ZlQW|zI;eGra-U_3JdLQn> Ua{E@jrW?wb;oCv{iaJ7m19~RA-v9sr delta 871 zcmYk5PiWLY6vi{jCYdC=$wG^GSWvtv>d~G|Ul8|F&^;&!g0R#o2yI1|Qa#9wfAC;0 zo^tE0h(|49JSdZ(2SKk23f?^^V*TH0>%3PJ%);)EkN3X$zS&)UBYkr#&5w!Xem)S% zPS(qJ%Vd{pIq+LQNF8u37uNM^2Cl&XzU$S#2x|3siK-7jb#a3E260QB23GH>GlJnr z2;a1tE?H<8O^R6?O=x@pY#e8>snr!>GsEbpu9kLfSx5&*X#6G+ljCjSpkoKtw7Oe5 zoqwyvatk-d;Dc5x&>Az0CDbF)!Z2HtR;iwpt*yyzy?E@_HB<#IuEy|LtLMUv4O5uc zCFv1-)x|q6GBP>72M3r`AfZ?pJL1n4J#Y1iwC1{Skf>#;!=GD`1S{6%gAQ zG1)tkHz|3=F(vg*Sb)7f_@z}Z3p~S=c#jO4nY|Uwkba1TsFVNUENrejm84@sb+ihb ziUStmxo!BO)yXVs8>T!%NjirgTAiyRZuBmE(rS*iyN2nzx|X%EJ9$dkMo;3UikR}` ztb=)omosAOwIVy%i}+zhOy_DAH}~)?i<>SlWQ<+t`d?OY{To%USYqbcD7{muw^_1~ z#y;;q-QVxa#OjK~%FoMW-x13^Gm97JFO3e&&Wwg - output.outputStream().use { outputStream -> - val buffer = ByteArray(64) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - cipher.update(buffer, 0, bytesRead)?.let { outputStream.write(it) } - } - cipher.doFinal()?.let { outputStream.write(it) } - } - } - } + fun createEncryptedZipFile(input: File, output: File, password: String) { + val zipParameters = ZipParameters() + zipParameters.isEncryptFiles = true + zipParameters.encryptionMethod = EncryptionMethod.AES + zipParameters.aesKeyStrength = AesKeyStrength.KEY_STRENGTH_256 - private fun getCipher(mode: Int, username: String, password: String): Cipher { - val iv: ByteArray = getSalt(username) - val aesKey: SecretKey = getAESKeyFromPassword(password, iv) - val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") - cipher.init(mode, aesKey, IvParameterSpec(iv)) + val filesToAdd = listOf(input) - return cipher + val zipFile = ZipFile(output, password.toCharArray()) + zipFile.addFiles(filesToAdd, zipParameters) } - internal fun getSalt(string: String): ByteArray { - val md = MessageDigest.getInstance("MD5") - md.reset() - md.update(string.toByteArray((StandardCharsets.UTF_8))) - return md.digest().slice(IntRange(0, 15)).toByteArray() - } + fun extractEncryptedZipFile(input: File, output: File, password: String) { + val zipFile = ZipFile(input, password.toCharArray()) + val allFiles = zipFile.fileHeaders - private fun getAESKeyFromPassword(password: String, salt: ByteArray?): SecretKey { - val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") - val iterationCount = 10000 - val keyLength = 256 - val spec: KeySpec = PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength) - return SecretKeySpec(factory.generateSecret(spec).encoded, "AES") + if (allFiles.size == 1) { + val targetFile = allFiles.first() + zipFile.extractFile(targetFile, output.parent, output.name) + } else { + throw D2Error.builder() + .errorComponent(D2ErrorComponent.SDK) + .errorDescription("Database zip file must contain a single file") + .errorCode(D2ErrorCode.DATABASE_IMPORT_INVALID_FILE) + .build() + } } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt b/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt deleted file mode 100644 index 8b8e9fcc40..0000000000 --- a/core/src/test/java/org/hisp/dhis/android/core/util/CipherUtilShould.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2004-2024, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.hisp.dhis.android.core.util - -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class CipherUtilShould { - @Test - fun create_16_long_salt() { - assertThat(CipherUtil.getSalt("password").size).isEqualTo(16) - assertThat(CipherUtil.getSalt("s").size).isEqualTo(16) - assertThat(CipherUtil.getSalt("veryverylongpasswordforuser").size).isEqualTo(16) - } -} From 1df9875cd94a250cb70e3a412d9baf59436bdea4 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 20 Mar 2024 12:37:01 +0100 Subject: [PATCH 195/222] [ANDROSDK-1837] TrackerLineList fails to evaluate filtered lastUpdated --- ...rackerLineListRepositoryEvaluatorShould.kt | 29 +++++++++++++++++++ .../trackerlinelist/TrackerLineListModel.kt | 20 ++++++------- .../evaluator/NotSupportedEvaluator.kt | 2 +- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt index a9be59f6fe..c0cb0d5938 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListRepositoryEvaluatorShould.kt @@ -295,6 +295,35 @@ internal class TrackerLineListRepositoryEvaluatorShould : BaseEvaluatorIntegrati assertThat(rows.flatten().map { it.value }).containsExactly("5", "10") } + @Test + fun evaluate_shared_properties() { + val event1 = generator.generate() + helper.createSingleEvent( + event1, + program.uid(), + programStage1.uid(), + orgunitChild1.uid(), + lastUpdated = DateUtils.SIMPLE_DATE_FORMAT.parse("2024-01-04"), + ) + + val result = d2.analyticsModule().trackerLineList() + .withEventOutput(programStage1.uid()) + .withColumn( + TrackerLineListItem.LastUpdated( + filters = listOf( + DateFilter.Range(startDate = "2024-01-01", endDate = "2024-01-31"), + ), + ), + ) + .withColumn(TrackerLineListItem.LastUpdatedBy) + .withColumn(TrackerLineListItem.CreatedBy) + .withColumn(TrackerLineListItem.OrganisationUnitItem()) + .blockingEvaluate() + + val rows = result.getOrThrow().rows + assertThat(rows.size).isEqualTo(1) + } + @Test fun evaluate_paging() { val event1 = generator.generate() diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt index 0a2268e044..7f79dee3b2 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/TrackerLineListModel.kt @@ -131,14 +131,14 @@ sealed class DataFilter { } internal object Label { - const val OrganisationUnit = "ou" - const val LastUpdated = "lastUpdated" - const val IncidentDate = "incidentDate" - const val EnrollmentDate = "enrollmentDate" - const val ScheduledDate = "scheduledDate" - const val EventDate = "eventDate" - const val CreatedBy = "createdBy" - const val LastUpdatedBy = "lastUpdatedBy" - const val ProgramStatus = "programStatus" - const val EventStatus = "eventStatus" + const val OrganisationUnit = "ouItem" + const val LastUpdated = "lastUpdatedItem" + const val IncidentDate = "incidentDateItem" + const val EnrollmentDate = "enrollmentDateItem" + const val ScheduledDate = "scheduledDateItem" + const val EventDate = "eventDateItem" + const val CreatedBy = "createdByItem" + const val LastUpdatedBy = "lastUpdatedByItem" + const val ProgramStatus = "programStatusItem" + const val EventStatus = "eventStatusItem" } diff --git a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt index 8c5b1081b5..3855aa6519 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/analytics/trackerlinelist/internal/evaluator/NotSupportedEvaluator.kt @@ -31,7 +31,7 @@ package org.hisp.dhis.android.core.analytics.trackerlinelist.internal.evaluator internal class NotSupportedEvaluator : TrackerLineListEvaluator() { override fun getCommonSelectSQL(): String { - return "Not supported" + return "'Not supported'" } override fun getCommonWhereSQL(): String { From 75ddef762ee0b224262d9d9d8b238dfc79be9978 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Fri, 22 Mar 2024 09:31:46 +0100 Subject: [PATCH 196/222] [ANDROSDK-1838] Adapt to changes in /icons endpoint --- .../icon/internal/CustomIconStoreIntegrationShould.kt | 3 ++- core/src/main/assets/migrations/160.sql | 2 +- core/src/main/assets/snapshots/snapshot.sql | 2 +- .../dhis/android/core/fileresource/FileResourceDomain.kt | 2 +- .../core/fileresource/FileResourceDownloadConst.kt | 2 +- .../dhis/android/core/fileresource/FileResourceRoutine.kt | 5 ++--- .../fileresource/internal/FileResourceDownloadCall.kt | 4 ++-- .../internal/FileResourceDownloadCallHelper.kt | 2 +- .../java/org/hisp/dhis/android/core/icon/CustomIcon.java | 8 ++++++-- .../hisp/dhis/android/core/icon/CustomIconTableInfo.kt | 4 ++-- .../dhis/android/core/icon/IconCollectionRepository.kt | 4 ++-- .../dhis/android/core/icon/internal/CustomIconFields.kt | 4 ++-- .../android/core/icon/internal/CustomIconStoreImpl.kt | 3 ++- .../hisp/dhis/android/core/data/icon/CustomIconSamples.kt | 3 ++- core/src/sharedTest/resources/icon/custom_icon.json | 4 +++- core/src/sharedTest/resources/icon/custom_icons.json | 8 ++++++-- .../org/hisp/dhis/android/core/icon/CustomIconShould.kt | 2 +- 17 files changed, 37 insertions(+), 25 deletions(-) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt index c12bf0625e..0bf7b94298 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreIntegrationShould.kt @@ -27,6 +27,7 @@ */ package org.hisp.dhis.android.core.icon.internal +import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.data.database.ObjectWithoutUidStoreAbstractIntegrationShould import org.hisp.dhis.android.core.data.icon.CustomIconSamples import org.hisp.dhis.android.core.icon.CustomIcon @@ -48,7 +49,7 @@ class CustomIconStoreIntegrationShould : override fun buildObjectToUpdate(): CustomIcon { return CustomIconSamples.getCustomIcon().toBuilder() - .fileResourceUid("otherResourceUid") + .fileResource(ObjectWithUid.create("otherResourceUid")) .build() } } diff --git a/core/src/main/assets/migrations/160.sql b/core/src/main/assets/migrations/160.sql index 146e7cd249..9a6e739630 100644 --- a/core/src/main/assets/migrations/160.sql +++ b/core/src/main/assets/migrations/160.sql @@ -1,3 +1,3 @@ # Add CustomIcon model (ANDROSDK-1630) -CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); \ No newline at end of file +CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResource TEXT NOT NULL, href TEXT NOT NULL); \ No newline at end of file diff --git a/core/src/main/assets/snapshots/snapshot.sql b/core/src/main/assets/snapshots/snapshot.sql index 93719f5894..115ac6003b 100644 --- a/core/src/main/assets/snapshots/snapshot.sql +++ b/core/src/main/assets/snapshots/snapshot.sql @@ -132,5 +132,5 @@ CREATE TABLE LatestAppVersion (_id INTEGER PRIMARY KEY AUTOINCREMENT, downloadUR CREATE TABLE ExpressionDimensionItem (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, expression TEXT); CREATE TABLE TrackerVisualization(_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT, description TEXT, displayDescription TEXT, type TEXT, outputType TEXT, program TEXT, programStage TEXT, trackedEntityType TEXT, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (trackedEntityType) REFERENCES TrackedEntityType (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); CREATE TABLE TrackerVisualizationDimension(_id INTEGER PRIMARY KEY AUTOINCREMENT, trackerVisualization TEXT NOT NULL, position TEXT NOT NULL, dimension TEXT NOT NULL, dimensionType TEXT, program TEXT, programStage TEXT, items TEXT, filter TEXT, repetition TEXT, FOREIGN KEY (trackerVisualization) REFERENCES TrackerVisualization (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (program) REFERENCES Program (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY (programStage) REFERENCES ProgramStage (uid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED); -CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResourceUid TEXT NOT NULL, href TEXT NOT NULL); +CREATE TABLE CustomIcon(_id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, fileResource TEXT NOT NULL, href TEXT NOT NULL); CREATE TABLE UserGroup (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, code TEXT, name TEXT, displayName TEXT, created TEXT, lastUpdated TEXT); \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt index cf931e9569..07a9e5f056 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDomain.kt @@ -34,6 +34,6 @@ enum class FileResourceDomain { MESSAGE_ATTACHMENT, USER_AVATAR, ORG_UNIT, - CUSTOM_ICON, + ICON, JOB_DATA, } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt index d34f57a01b..f6b6bfbb75 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceDownloadConst.kt @@ -46,5 +46,5 @@ enum class FileResourceDataDomainType { enum class FileResourceDomainType(internal val domainType: FileResourceDomain) { DATA_VALUE(FileResourceDomain.DATA_VALUE), - CUSTOM_ICON(FileResourceDomain.CUSTOM_ICON), + ICON(FileResourceDomain.ICON), } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt index 33d7e91c5d..1e4f4c780f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/FileResourceRoutine.kt @@ -35,7 +35,6 @@ import org.hisp.dhis.android.core.dataelement.DataElementCollectionRepository import org.hisp.dhis.android.core.datavalue.DataValue import org.hisp.dhis.android.core.datavalue.DataValueCollectionRepository import org.hisp.dhis.android.core.fileresource.internal.FileResourceStore -import org.hisp.dhis.android.core.icon.CustomIcon import org.hisp.dhis.android.core.icon.internal.CustomIconStore import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeCollectionRepository @@ -92,14 +91,14 @@ internal class FileResourceRoutine( val fileResourceUids = dataValues.map(DataValue::value) + trackedEntityAttributeValues.map(TrackedEntityAttributeValue::value) + trackedEntityDataValues.map(TrackedEntityDataValue::value) + - customIcons.map(CustomIcon::fileResourceUid) + customIcons.map { it.fileResource().uid() } val calendar = Calendar.getInstance().apply { add(Calendar.HOUR_OF_DAY, -2) } val fileResources = fileResourceCollectionRepository .byUid().notIn(fileResourceUids.mapNotNull { it }) - .byDomain().`in`(FileResourceDomain.DATA_VALUE, FileResourceDomain.CUSTOM_ICON) + .byDomain().`in`(FileResourceDomain.DATA_VALUE, FileResourceDomain.ICON) .byLastUpdated().before(after ?: calendar.time) .blockingGet() diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt index d7b0c7d851..b4b7b1d824 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCall.kt @@ -163,7 +163,7 @@ internal class FileResourceDownloadCall( } private suspend fun downloadCustomIcons(params: FileResourceDownloadParams, existingFileResources: List) { - if (params.domainTypes.contains(FileResourceDomainType.CUSTOM_ICON)) { + if (params.domainTypes.contains(FileResourceDomainType.ICON)) { val iconKeys: List = helper.getMissingCustomIcons(existingFileResources) downloadAndPersistFiles( @@ -172,7 +172,7 @@ internal class FileResourceDownloadCall( download = { v -> fileResourceService.getCustomIcon(v.href()) }, - getUid = { v -> v.fileResourceUid() }, + getUid = { v -> v.fileResource().uid() }, ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt index 51d44cbc3a..197f76810f 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/fileresource/internal/FileResourceDownloadCallHelper.kt @@ -126,7 +126,7 @@ internal class FileResourceDownloadCallHelper( existingFileResources: List, ): List { val customIconsWhereClause = WhereClauseBuilder() - .appendNotInKeyStringValues(CustomIconTableInfo.Columns.FILE_RESOURCE_UID, existingFileResources) + .appendNotInKeyStringValues(CustomIconTableInfo.Columns.FILE_RESOURCE, existingFileResources) .build() return customIconStore.selectWhere(customIconsWhereClause) } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java index 4e0b76a409..0ef6827c4e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIcon.java @@ -35,9 +35,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.gabrielittner.auto.value.cursor.ColumnAdapter; import com.google.auto.value.AutoValue; +import org.hisp.dhis.android.core.arch.db.adapters.identifiable.internal.ObjectWithUidColumnAdapter; import org.hisp.dhis.android.core.common.CoreObject; +import org.hisp.dhis.android.core.common.ObjectWithUid; @AutoValue @JsonDeserialize(builder = $$AutoValue_CustomIcon.Builder.class) @@ -49,7 +52,8 @@ public abstract class CustomIcon implements CoreObject { @NonNull @JsonProperty - public abstract String fileResourceUid(); + @ColumnAdapter(ObjectWithUidColumnAdapter.class) + public abstract ObjectWithUid fileResource(); @NonNull @JsonProperty @@ -74,7 +78,7 @@ public abstract static class Builder { public abstract Builder key(String key); - public abstract Builder fileResourceUid(String fileResourceUid); + public abstract Builder fileResource(ObjectWithUid fileResource); public abstract Builder href(String href); diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt index 9365fff257..bb1b6d4438 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/CustomIconTableInfo.kt @@ -50,7 +50,7 @@ object CustomIconTableInfo { return CollectionsHelper.appendInNewArray( super.all(), KEY, - FILE_RESOURCE_UID, + FILE_RESOURCE, HREF, ) } @@ -64,7 +64,7 @@ object CustomIconTableInfo { companion object { const val KEY = "key" - const val FILE_RESOURCE_UID = "fileResourceUid" + const val FILE_RESOURCE = "fileResource" const val HREF = "href" } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt index 8a353def64..49d06d8f14 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/IconCollectionRepository.kt @@ -51,8 +51,8 @@ class IconCollectionRepository internal constructor( Icon.Default(key) } else { customIconStore.selectByKey(key)?.let { customIcon -> - val fileResource = fileResourceStore.selectByUid(customIcon.fileResourceUid()) - Icon.Custom(customIcon.key(), customIcon.fileResourceUid(), fileResource?.path()) + val fileResource = fileResourceStore.selectByUid(customIcon.fileResource().uid()) + Icon.Custom(customIcon.key(), customIcon.fileResource().uid(), fileResource?.path()) } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt index d59ea88b1d..7658d5e994 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconFields.kt @@ -33,7 +33,7 @@ import org.hisp.dhis.android.core.icon.CustomIcon internal object CustomIconFields { private const val KEY = "key" - private const val FILE_RESOURCE_UID = "fileResourceUid" + private const val FILE_RESOURCE = "fileResource" private const val HREF = "href" private val fh = FieldsHelper() @@ -42,7 +42,7 @@ internal object CustomIconFields { Fields.builder() .fields( fh.field(KEY), - fh.field(FILE_RESOURCE_UID), + fh.nestedFieldWithUid(FILE_RESOURCE), fh.field(HREF), ) .build() diff --git a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt index a552ee986d..eb47622532 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/icon/internal/CustomIconStoreImpl.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementBinde import org.hisp.dhis.android.core.arch.db.stores.binders.internal.StatementWrapper import org.hisp.dhis.android.core.arch.db.stores.binders.internal.WhereStatementBinder import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectWithoutUidStoreImpl +import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidOrNull import org.hisp.dhis.android.core.icon.CustomIcon import org.hisp.dhis.android.core.icon.CustomIconTableInfo import org.koin.core.annotation.Singleton @@ -62,7 +63,7 @@ internal class CustomIconStoreImpl( companion object { private val BINDER = StatementBinder { o: CustomIcon, w: StatementWrapper -> w.bind(1, o.key()) - w.bind(2, o.fileResourceUid()) + w.bind(2, getUidOrNull(o.fileResource())) w.bind(3, o.href()) } diff --git a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt index c37a7badf5..ea568c35d0 100644 --- a/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt +++ b/core/src/sharedTest/java/org/hisp/dhis/android/core/data/icon/CustomIconSamples.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.data.icon +import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.icon.CustomIcon object CustomIconSamples { @@ -36,7 +37,7 @@ object CustomIconSamples { return CustomIcon.builder() .id(1L) .key("childIcon") - .fileResourceUid("lNrwSpIy1Q9") + .fileResource(ObjectWithUid.create("lNrwSpIy1Q9")) .href("https://play.im.dhis2.org/dev/api/icons/childIcon/icon") .build() } diff --git a/core/src/sharedTest/resources/icon/custom_icon.json b/core/src/sharedTest/resources/icon/custom_icon.json index fe50d75a13..ce4aa5dec5 100644 --- a/core/src/sharedTest/resources/icon/custom_icon.json +++ b/core/src/sharedTest/resources/icon/custom_icon.json @@ -1,5 +1,7 @@ { "key": "childIcon", - "fileResourceUid": "lNrwSpIy1Q9", + "fileResource": { + "id": "lNrwSpIy1Q9" + }, "href": "https://play.im.dhis2.org/dev/api/icons/childIcon/icon" } \ No newline at end of file diff --git a/core/src/sharedTest/resources/icon/custom_icons.json b/core/src/sharedTest/resources/icon/custom_icons.json index 8691b85e92..4bef38c95a 100644 --- a/core/src/sharedTest/resources/icon/custom_icons.json +++ b/core/src/sharedTest/resources/icon/custom_icons.json @@ -8,12 +8,16 @@ "icons": [ { "key": "antenatal_icon", - "fileResourceUid": "lNrwSpIy1Q9", + "fileResource": { + "id": "lNrwSpIy1Q9" + }, "href": "https://play.im.dhis2.org/dev/api/icons/antenatal_icon/icon" }, { "key": "visit_icon", - "fileResourceUid": "yx5Vm0DBjFr", + "fileResource": { + "id": "yx5Vm0DBjFr" + }, "href": "https://play.im.dhis2.org/dev/api/icons/visit_icon/icon" } ] diff --git a/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt b/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt index 4f32c4ce56..fd38a904d8 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/icon/CustomIconShould.kt @@ -41,7 +41,7 @@ class CustomIconShould : val icon = objectMapper.readValue(jsonStream, CustomIcon::class.java) assertThat(icon.key()).isEqualTo("childIcon") - assertThat(icon.fileResourceUid()).isEqualTo("lNrwSpIy1Q9") + assertThat(icon.fileResource().uid()).isEqualTo("lNrwSpIy1Q9") assertThat(icon.href()).isEqualTo("https://play.im.dhis2.org/dev/api/icons/childIcon/icon") } } From 6d9dac648259bdf54c12d21d91c7014f052f1624 Mon Sep 17 00:00:00 2001 From: Victor Garcia Date: Wed, 3 Apr 2024 08:37:50 +0200 Subject: [PATCH 197/222] [ANDROSDK-1642-PUBLISH-JACOCO] Refactor maven publish and jacoco plugins --- .idea/gradle.xml | 12 ++ build.gradle.kts | 1 - buildSrc/build.gradle.kts | 40 +++++ buildSrc/settings.gradle.kts | 35 ++++ buildSrc/src/main/kotlin/Props.kt | 44 +++++ .../main/kotlin/jacoco-conventions.gradle.kts | 40 ++++- .../maven-publish-conventions.gradle.kts | 150 ++++++++++++++++++ core/build.gradle.kts | 5 +- core/gradle.properties | 19 --- core/plugins/gradle-mvn-push.gradle | 141 ---------------- gradle/libs.versions.toml | 4 +- instrumented-test-app/build.gradle.kts | 3 +- 12 files changed, 323 insertions(+), 171 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/Props.kt rename core/plugins/jacoco.gradle.kts => buildSrc/src/main/kotlin/jacoco-conventions.gradle.kts (70%) create mode 100644 buildSrc/src/main/kotlin/maven-publish-conventions.gradle.kts delete mode 100644 core/plugins/gradle-mvn-push.gradle diff --git a/.idea/gradle.xml b/.idea/gradle.xml index b77a8dcd0d..830a0eeb1d 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,11 +4,23 @@

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%0DM-|cY!+kgf9(F75HfYd#yg;#47y2;xPJ$jj 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