diff --git a/core/api/core.api b/core/api/core.api index faf68ff043..0842f50155 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -11006,9 +11006,16 @@ public abstract interface class org/hisp/dhis/android/core/relationship/Relation } public abstract interface class org/hisp/dhis/android/core/relationship/RelationshipService { + public abstract fun getRelationshipTypesForEnrollments (Ljava/lang/String;)Ljava/util/List; + public abstract fun getRelationshipTypesForEvents (Ljava/lang/String;)Ljava/util/List; + public abstract fun getRelationshipTypesForTrackedEntities (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List; public abstract fun hasAccessPermission (Lorg/hisp/dhis/android/core/relationship/RelationshipType;)Z } +public final class org/hisp/dhis/android/core/relationship/RelationshipService$DefaultImpls { + public static synthetic fun getRelationshipTypesForTrackedEntities$default (Lorg/hisp/dhis/android/core/relationship/RelationshipService;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/util/List; +} + public final class org/hisp/dhis/android/core/relationship/RelationshipTableInfo { public static final field TABLE_INFO Lorg/hisp/dhis/android/core/arch/db/tableinfos/TableInfo; } @@ -11072,6 +11079,19 @@ public class org/hisp/dhis/android/core/relationship/RelationshipTypeTableInfo$C public fun all ()[Ljava/lang/String; } +public final class org/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide { + public fun (Lorg/hisp/dhis/android/core/relationship/RelationshipType;Lorg/hisp/dhis/android/core/relationship/RelationshipConstraintType;)V + public final fun component1 ()Lorg/hisp/dhis/android/core/relationship/RelationshipType; + public final fun component2 ()Lorg/hisp/dhis/android/core/relationship/RelationshipConstraintType; + public final fun copy (Lorg/hisp/dhis/android/core/relationship/RelationshipType;Lorg/hisp/dhis/android/core/relationship/RelationshipConstraintType;)Lorg/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide; + public static synthetic fun copy$default (Lorg/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide;Lorg/hisp/dhis/android/core/relationship/RelationshipType;Lorg/hisp/dhis/android/core/relationship/RelationshipConstraintType;ILjava/lang/Object;)Lorg/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide; + public fun equals (Ljava/lang/Object;)Z + public final fun getEntitySide ()Lorg/hisp/dhis/android/core/relationship/RelationshipConstraintType; + public final fun getRelationshipType ()Lorg/hisp/dhis/android/core/relationship/RelationshipType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract class org/hisp/dhis/android/core/relationship/TrackerDataView : org/hisp/dhis/android/core/common/BaseObject { public fun ()V public abstract fun attributes ()Ljava/util/List; diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/RelationshipServiceIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/RelationshipServiceIntegrationShould.kt new file mode 100644 index 0000000000..9cfef8137f --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/relationship/RelationshipServiceIntegrationShould.kt @@ -0,0 +1,78 @@ +/* + * 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 + +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) +class RelationshipServiceIntegrationShould : BaseMockIntegrationTestFullDispatcher() { + + @Test + fun getAvailableRelationshipsForTrackedEntities() { + val relationshipsWithoutProgram = d2.relationshipModule().relationshipService() + .getRelationshipTypesForTrackedEntities("nEenWmSyUEp") + + assertThat(relationshipsWithoutProgram.size).isEqualTo(2) + assertThat(relationshipsWithoutProgram[0].relationshipType.uid()).isEqualTo("WiH6923nMtb") + assertThat(relationshipsWithoutProgram[0].entitySide).isEqualTo(RelationshipConstraintType.TO) + assertThat(relationshipsWithoutProgram[1].relationshipType.uid()).isEqualTo("V2kkHafqs8G") + assertThat(relationshipsWithoutProgram[1].entitySide).isEqualTo(RelationshipConstraintType.FROM) + + val relationshipsWithProgram = d2.relationshipModule().relationshipService() + .getRelationshipTypesForTrackedEntities("nEenWmSyUEp", "other_program") + + assertThat(relationshipsWithProgram.size).isEqualTo(1) + assertThat(relationshipsWithoutProgram[0].relationshipType.uid()).isEqualTo("WiH6923nMtb") + assertThat(relationshipsWithoutProgram[0].entitySide).isEqualTo(RelationshipConstraintType.TO) + } + + @Test + fun getAvailableRelationshipsForEnrollments() { + val relationships = d2.relationshipModule().relationshipService() + .getRelationshipTypesForEnrollments("IpHINAT79UW") + + assertThat(relationships.size).isEqualTo(1) + assertThat(relationships[0].relationshipType.uid()).isEqualTo("WiH6923nMtb") + assertThat(relationships[0].entitySide).isEqualTo(RelationshipConstraintType.FROM) + } + + @Test + fun getAvailableRelationshipsForEvents() { + val relationships = d2.relationshipModule().relationshipService() + .getRelationshipTypesForEvents("dBwrot7S420") + + assertThat(relationships.size).isEqualTo(1) + assertThat(relationships[0].relationshipType.uid()).isEqualTo("o51cUNONthg") + assertThat(relationships[0].entitySide).isEqualTo(RelationshipConstraintType.FROM) + } +} diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipService.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipService.kt index 63a492aa03..7bc69fce30 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipService.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipService.kt @@ -30,4 +30,39 @@ package org.hisp.dhis.android.core.relationship interface RelationshipService { fun hasAccessPermission(relationshipType: RelationshipType): Boolean + + /** + * Returns all the relationship types relevant to the given trackedEntityType and program (optional). + * The list includes the side of the TrackedEntity (FROM or TO). If the TrackedEntity might be in both sides, + * the list will include two entries for the same relationship type, one for each side. + * + * @param trackedEntityType the trackedEntityType uid + * @param programUid optional program uid + */ + fun getRelationshipTypesForTrackedEntities( + trackedEntityType: String, + programUid: String? = null, + ): List + + /** + * Returns all the relationship types relevant to the given enrollment (program uid). + * The list includes the side of the Enrollment (FROM or TO). If the Enrollment might be in both sides, + * the list will include two entries for the same relationship type, one for each side. + * + * @param programUid the program uid + */ + fun getRelationshipTypesForEnrollments( + programUid: String, + ): List + + /** + * Returns all the relationship types relevant to the given event (program stage uid). + * The list includes the side of the Event (FROM or TO). If the Event might be in both sides, + * the list will include two entries for the same relationship type, one for each side. + * + * @param programStageUid the program stage uid + */ + fun getRelationshipTypesForEvents( + programStageUid: String, + ): List } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipServiceImpl.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipServiceImpl.kt index edbe7385ff..23384c6f59 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipServiceImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipServiceImpl.kt @@ -38,6 +38,7 @@ internal class RelationshipServiceImpl( private val programRepository: ProgramCollectionRepository, private val programStageRepository: ProgramStageCollectionRepository, private val trackedEntityTypeRepository: TrackedEntityTypeCollectionRepository, + private val relationshipTypeRepository: RelationshipTypeCollectionRepository, ) : RelationshipService { override fun hasAccessPermission(relationshipType: RelationshipType): Boolean { val fromAccess = relationshipType.fromConstraint()?.let { constraintAccess(it) } ?: false @@ -52,6 +53,57 @@ internal class RelationshipServiceImpl( return writeAccess } + override fun getRelationshipTypesForTrackedEntities( + trackedEntityType: String, + programUid: String?, + ): List { + val entityType = RelationshipEntityType.TRACKED_ENTITY_INSTANCE + + val potentialRelTypes = relationshipTypeRepository + .byConstraint(entityType, trackedEntityType) + .withConstraints() + .blockingGet() + + return mapToApplicableSides(entityType, potentialRelTypes) { constraint -> + constraint.trackedEntityType()?.uid() == trackedEntityType && + ( + programUid == null || + constraint.program()?.uid() == null || + constraint.program()?.uid() == programUid + ) + } + } + + override fun getRelationshipTypesForEnrollments( + programUid: String, + ): List { + val entityType = RelationshipEntityType.PROGRAM_INSTANCE + + val potentialRelTypes = relationshipTypeRepository + .byConstraint(entityType, programUid) + .withConstraints() + .blockingGet() + + return mapToApplicableSides(entityType, potentialRelTypes) { constraint -> + constraint.program()?.uid() == programUid + } + } + + override fun getRelationshipTypesForEvents( + programStageUid: String, + ): List { + val entityType = RelationshipEntityType.PROGRAM_STAGE_INSTANCE + + val potentialRelTypes = relationshipTypeRepository + .byConstraint(entityType, programStageUid) + .withConstraints() + .blockingGet() + + return mapToApplicableSides(entityType, potentialRelTypes) { constraint -> + constraint.programStage()?.uid() == programStageUid + } + } + private fun constraintAccess( constraint: RelationshipConstraint, ): Boolean = when (constraint.relationshipEntity()) { @@ -59,6 +111,7 @@ internal class RelationshipServiceImpl( val programUid = constraint.program()?.uid() programRepository.uid(programUid).blockingGet()!!.access().data().write()!! } + RelationshipEntityType.PROGRAM_STAGE_INSTANCE -> { if (constraint.programStage()?.uid() != null) { val programStageUid = constraint.programStage()?.uid() @@ -68,10 +121,39 @@ internal class RelationshipServiceImpl( programRepository.uid(programUid).blockingGet()!!.access().data().write()!! } } + RelationshipEntityType.TRACKED_ENTITY_INSTANCE -> { val teTypeUid = constraint.trackedEntityType()?.uid() trackedEntityTypeRepository.uid(teTypeUid).blockingGet()!!.access().data().write()!! } + else -> false } + + private fun mapToApplicableSides( + entityType: RelationshipEntityType, + relationshipTypes: List, + matchesSide: (RelationshipConstraint) -> Boolean, + ): List { + return relationshipTypes.flatMap { relType -> + val applicableSides = mutableListOf() + if (matchesConstraint(relType.fromConstraint(), entityType, matchesSide)) { + applicableSides.add(RelationshipTypeWithEntitySide(relType, RelationshipConstraintType.FROM)) + } + if (relType.bidirectional() == true && matchesConstraint(relType.toConstraint(), entityType, matchesSide)) { + applicableSides.add(RelationshipTypeWithEntitySide(relType, RelationshipConstraintType.TO)) + } + applicableSides + } + } + + private fun matchesConstraint( + constraint: RelationshipConstraint?, + entityType: RelationshipEntityType, + matchesSide: (RelationshipConstraint) -> Boolean, + ): Boolean { + return constraint?.let { + constraint.relationshipEntity() == entityType && matchesSide(constraint) + } ?: false + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide.kt b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide.kt new file mode 100644 index 0000000000..32db892feb --- /dev/null +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/RelationshipTypeWithEntitySide.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.relationship + +data class RelationshipTypeWithEntitySide( + val relationshipType: RelationshipType, + val entitySide: RelationshipConstraintType, +) diff --git a/core/src/sharedTest/resources/relationship/relationship_types.json b/core/src/sharedTest/resources/relationship/relationship_types.json index 02d933a8d8..598f8b834e 100644 --- a/core/src/sharedTest/resources/relationship/relationship_types.json +++ b/core/src/sharedTest/resources/relationship/relationship_types.json @@ -65,8 +65,8 @@ }, "toConstraint": { "relationshipEntity": "PROGRAM_STAGE_INSTANCE", - "program": { - "id": "lxAQ7Zs9VYR" + "programStage": { + "id": "dBwrot7S420" } } }, @@ -91,8 +91,8 @@ }, "fromConstraint": { "relationshipEntity": "PROGRAM_STAGE_INSTANCE", - "program": { - "id": "lxAQ7Zs9VYR" + "programStage": { + "id": "dBwrot7S420" } }, "toConstraint": { diff --git a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipServiceShould.kt b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipServiceShould.kt index 898c8dfc05..fe0c4fa2e9 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipServiceShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/relationship/RelationshipServiceShould.kt @@ -27,6 +27,7 @@ class RelationshipServiceShould { mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) private val trackedEntityTypeRepository: TrackedEntityTypeCollectionRepository = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private val relationshipTypeRepository: RelationshipTypeCollectionRepository = mock() private lateinit var relationshipService: RelationshipService @Before @@ -35,6 +36,7 @@ class RelationshipServiceShould { programRepository, programStageRepository, trackedEntityTypeRepository, + relationshipTypeRepository, ) }