diff --git a/app/src/main/java/org/dhis2/bindings/Extensions.kt b/app/src/main/java/org/dhis2/bindings/Extensions.kt index b9febcd56c..ae4ea9c1a9 100644 --- a/app/src/main/java/org/dhis2/bindings/Extensions.kt +++ b/app/src/main/java/org/dhis2/bindings/Extensions.kt @@ -20,6 +20,7 @@ import java.text.DecimalFormat fun MutableLiveData.default(initialValue: T) = this.apply { setValue(initialValue) } +@Deprecated("Use ProfilePictureProvider instead") fun TrackedEntityInstance.profilePicturePath(d2: D2, programUid: String?): String { var path: String? = null diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 377fff6329..bebd188c08 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -33,6 +33,7 @@ import org.dhis2.metadata.usecases.FileResourceConfiguration; import org.dhis2.metadata.usecases.ProgramConfiguration; import org.dhis2.metadata.usecases.TrackedEntityInstanceConfiguration; +import org.dhis2.tracker.data.ProfilePictureProvider; import org.dhis2.tracker.relationships.model.RelationshipDirection; import org.dhis2.tracker.relationships.model.RelationshipModel; import org.dhis2.tracker.relationships.model.RelationshipOwnerType; @@ -121,6 +122,7 @@ public class SearchRepositoryImpl implements SearchRepository { private HashMap> trackedEntityTypeAttributesUidsCache = new HashMap(); private final MetadataIconProvider metadataIconProvider; + private final ProfilePictureProvider profilePictureProvider; SearchRepositoryImpl(String teiType, @Nullable String initialProgram, @@ -134,7 +136,8 @@ public class SearchRepositoryImpl implements SearchRepository { NetworkUtils networkUtils, SearchTEIRepository searchTEIRepository, ThemeManager themeManager, - MetadataIconProvider metadataIconProvider + MetadataIconProvider metadataIconProvider, + ProfilePictureProvider profilePictureProvider ) { this.teiType = teiType; this.d2 = d2; @@ -155,6 +158,7 @@ public class SearchRepositoryImpl implements SearchRepository { currentProgram, resources); this.metadataIconProvider = metadataIconProvider; + this.profilePictureProvider = profilePictureProvider; } @@ -746,7 +750,7 @@ public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Pr } else { searchTei.setEnrolledOrgUnit(orgUnitName(searchTei.getTei().organisationUnit())); } - searchTei.setProfilePicture(profilePicturePath(dbTei, selectedProgram)); + searchTei.setProfilePicture(profilePictureProvider.invoke(dbTei, selectedProgram != null ? selectedProgram.uid() : null)); } else { searchTei.setTei(teiFromItem); searchTei.setEnrolledOrgUnit(orgUnitName(searchTei.getTei().organisationUnit())); diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java index 89c30cd2f3..ca9c9907fc 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java @@ -155,6 +155,7 @@ SearchRepository searchRepository(@NonNull D2 d2, SearchTEIRepository searchTEIRepository, ThemeManager themeManager, MetadataIconProvider metadataIconProvider) { + ProfilePictureProvider profilePictureProvider = new ProfilePictureProvider(d2); return new SearchRepositoryImpl(teiType, initialProgram, d2, @@ -167,7 +168,8 @@ SearchRepository searchRepository(@NonNull D2 d2, networkUtils, searchTEIRepository, themeManager, - metadataIconProvider); + metadataIconProvider, + profilePictureProvider); } @Provides diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt index 7dfb99982c..296767dc0d 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt @@ -1,25 +1,63 @@ package org.dhis2.usescases.searchTrackEntity +import dhis2.org.analytics.charts.Charts import kotlinx.coroutines.Dispatchers +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.Filters +import org.dhis2.commons.filters.data.FilterPresenter +import org.dhis2.commons.filters.sorting.SortingItem +import org.dhis2.commons.network.NetworkUtils +import org.dhis2.commons.reporting.CrashReportController +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.forms.dataentry.SearchTEIRepository +import org.dhis2.data.sorting.SearchSortingValueSetter import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.FieldUiModelImpl import org.dhis2.form.model.UiRenderType import org.dhis2.form.ui.FieldViewModelFactory +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.ui.ThemeManager import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.filters.internal.BooleanFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.EnumFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentCollectionRepository +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventCollectionRepository +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.android.core.organisationunit.OrganisationUnitCollectionRepository +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramCollectionRepository import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeCollectionRepository +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.Calendar +import java.util.Date class SearchRepositoryTest { @@ -30,6 +68,34 @@ class SearchRepositoryTest { on { io() } doReturn Dispatchers.IO } private lateinit var searchRepository: SearchRepositoryImplKt + private lateinit var searchRepositoryJava: SearchRepository + + private val trackedEntitySearchItemHelper: TrackedEntitySearchItemHelper = mock() + + private val enrollmentCollectionRepository: EnrollmentCollectionRepository = mock() + private val stringFilterConnector: StringFilterConnector = mock() + private val booleanFilterConnector: BooleanFilterConnector = mock() + + private val programCollectionRepository: ProgramCollectionRepository = mock() + private val programReadOnlyOneObjectRepository: ReadOnlyOneObjectRepositoryFinalImpl = mock() + + private val eventCollectionRepository: EventCollectionRepository = mock() + private val enumEventFilterConnector: EnumFilterConnector = mock() + private val stringEventFilterConnector: StringFilterConnector = mock() + + private val orgUnitCollectionRepository: OrganisationUnitCollectionRepository = mock() + private val readOnlyOneObjectRepository: ReadOnlyOneObjectRepositoryFinalImpl = mock() + + private val filterPresenter: FilterPresenter = mock() + private val resourceManager: ResourceManager = mock() + private val sortingValueSetter: SearchSortingValueSetter = mock() + private val dhisPeriodUtils: DhisPeriodUtils = mock() + private val charts: Charts = mock() + private val crashReporterController: CrashReportController = mock() + private val networkUtils: NetworkUtils = mock() + private val searchTEIRepository: SearchTEIRepository = mock() + private val themeManager: ThemeManager = mock() + private val profilePictureProvider: ProfilePictureProvider = mock() @Before fun setUp() { @@ -56,6 +122,23 @@ class SearchRepositoryTest { trackedEntityInstanceInfoProvider = mock(), eventInfoProvider = mock(), ) + + searchRepositoryJava = SearchRepositoryImpl( + "teiType", + null, + d2, + filterPresenter, + resourceManager, + sortingValueSetter, + dhisPeriodUtils, + charts, + crashReporterController, + networkUtils, + searchTEIRepository, + themeManager, + metadataIconProvider, + profilePictureProvider, + ) } @Test @@ -75,6 +158,241 @@ class SearchRepositoryTest { assertEquals("state", sortedData[9].uid) } + @Test + fun shouldTransformToSearchTeiModelWithOverdueEvents() { + val searchItem = getTrackedEntitySearchItem("header") + val program = Program.builder().uid("programUid").build() + val sorting = SortingItem.create(Filters.ENROLLMENT_DATE) + val tei = TrackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem) + + val overdueDate = DateUtils.getInstance().getCalendarByDate(Date()) + overdueDate.add(Calendar.DATE, -2) + + val enrollmentsInProgram = listOf( + createEnrollment("enrollmentUid", "orgUnit", program.uid()), + createEnrollment("enrollmentUid_2", "orgUnit", program.uid()), + ) + val allEnrollments = listOf( + createEnrollment("enrollmentUid_3", "orgUnit", "uid"), + createEnrollment("enrollmentUid_4", "orgUnit_2", "uid"), + ) + val events = listOf( + createEvent("eventUid", EventStatus.OVERDUE, overdueDate.time), + createEvent("eventUid", EventStatus.SCHEDULE, Date()), + ) + + mockedSdkCalls(searchItem, tei, enrollmentsInProgram, allEnrollments, events) + + val result = searchRepositoryJava.transform(searchItem, program, true, sorting) + + assertTrue(result.isHasOverdue) + } + + @Test + fun shouldTransformToSearchTeiModelWithOutOverdueEvents() { + val searchItem = getTrackedEntitySearchItem("header") + val program = Program.builder().uid("programUid").build() + val sorting = SortingItem.create(Filters.ENROLLMENT_DATE) + val tei = TrackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem) + + val overdueDate = DateUtils.getInstance().getCalendarByDate(Date()) + overdueDate.add(Calendar.DATE, -2) + + val enrollmentsInProgram = listOf( + createEnrollment("enrollmentUid", "orgUnit", program.uid()), + createEnrollment("enrollmentUid_2", "orgUnit", program.uid()), + ) + val allEnrollments = listOf( + createEnrollment("enrollmentUid_3", "orgUnit", "uid"), + createEnrollment("enrollmentUid_4", "orgUnit_2", "uid"), + ) + val events = listOf( + createEvent("eventUid", EventStatus.SCHEDULE, Date()), + ) + + mockedSdkCalls(searchItem, tei, enrollmentsInProgram, allEnrollments, events) + + val result = searchRepositoryJava.transform(searchItem, program, true, sorting) + + assertFalse(result.isHasOverdue) + } + + private fun mockedSdkCalls( + searchItem: TrackedEntitySearchItem, + teiToReturn: TrackedEntityInstance, + enrollmentsInProgramToReturn: List = listOf(), + enrollmentsForInfoToReturn: List = listOf(), + eventsToReturn: List = listOf(), + profilePathToReturn: String = "", + orgUnitCount: Int = 1, + ) { + whenever( + trackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem), + ) doReturn teiToReturn + + if (searchItem.isOnline) { + whenever(d2.trackedEntityModule().trackedEntityInstances()) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() + .uid(any()), + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() + .uid(any()) + .blockingGet(), + ) doReturn teiToReturn + } + + whenever(d2.enrollmentModule().enrollments()) doReturn mock() + whenever( + d2.enrollmentModule().enrollments() + .byTrackedEntityInstance(), + ) doReturn mock() + whenever( + d2.enrollmentModule().enrollments() + .byTrackedEntityInstance().eq(any()), + ) doReturn enrollmentCollectionRepository + + whenever( + enrollmentCollectionRepository.byProgram(), + ) doReturn stringFilterConnector + whenever( + stringFilterConnector.eq(any()), + ) doReturn enrollmentCollectionRepository + + whenever( + enrollmentCollectionRepository.byProgram().eq(any()), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.orderByEnrollmentDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.blockingGet(), + ) doReturn enrollmentsInProgramToReturn + + // Mock setEnrollmentInfo + whenever( + enrollmentCollectionRepository.byDeleted(), + ) doReturn booleanFilterConnector + whenever( + booleanFilterConnector.eq(any()), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.byDeleted().eq(false), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.orderByCreated(RepositoryScope.OrderByDirection.DESC), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.blockingGet(), + ) doReturn enrollmentsForInfoToReturn + + val programUid = if (enrollmentsForInfoToReturn.isNotEmpty()) enrollmentsForInfoToReturn[0].program() else "programUid" + whenever(d2.programModule().programs()) doReturn programCollectionRepository + whenever( + programCollectionRepository.uid(any()), + ) doReturn programReadOnlyOneObjectRepository + whenever( + programReadOnlyOneObjectRepository.blockingGet(), + ) doReturn Program.builder().uid(programUid).displayFrontPageList(true).build() + + // Mock setOverdueEvents + whenever(d2.eventModule().events()) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byEnrollmentUid(), + ) doReturn stringEventFilterConnector + whenever( + stringEventFilterConnector.`in`(any>()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byEnrollmentUid().`in`(any>()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byStatus(), + ) doReturn enumEventFilterConnector + whenever( + enumEventFilterConnector.eq(EventStatus.OVERDUE), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byStatus().eq(EventStatus.OVERDUE), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byProgramUid(), + ) doReturn stringEventFilterConnector + whenever( + eventCollectionRepository.byProgramUid().eq(any()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.orderByDueDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.blockingGet(), + ) doReturn eventsToReturn.filter { it.status() == EventStatus.OVERDUE } + + // mock orgUnitName(orgUnitUid) + whenever(d2.organisationUnitModule().organisationUnits()) doReturn orgUnitCollectionRepository + whenever( + orgUnitCollectionRepository.uid(any()), + ) doReturn readOnlyOneObjectRepository + whenever(readOnlyOneObjectRepository.blockingGet()) doReturn OrganisationUnit.builder().uid("uid").displayName("orgUnitName").build() + + whenever(profilePictureProvider.invoke(any(), any())) doReturn profilePathToReturn + + // mock displayOrgUnit() + whenever( + orgUnitCollectionRepository.byProgramUids(any()), + ) doReturn orgUnitCollectionRepository + whenever( + orgUnitCollectionRepository.blockingCount(), + ) doReturn orgUnitCount + } + + private fun getTrackedEntitySearchItem( + header: String?, + isOnline: Boolean = false, + state: State = State.SYNCED, + attributesValues: List = listOf(), + ): TrackedEntitySearchItem { + return TrackedEntitySearchItem( + uid = "uid", + created = Date(), + lastUpdated = Date(), + createdAtClient = Date(), + lastUpdatedAtClient = Date(), + organisationUnit = "orgUnit", + geometry = null, + syncState = state, + aggregatedSyncState = state, + deleted = false, + isOnline = isOnline, + type = TrackedEntityType.builder().uid("uid").build(), + header = header, + attributeValues = attributesValues, + ) + } + + private fun createEnrollment( + uid: String, + orgUnitUid: String, + programUid: String, + status: EnrollmentStatus = EnrollmentStatus.ACTIVE, + ) = + Enrollment.builder() + .uid(uid) + .organisationUnit(orgUnitUid) + .program(programUid) + .status(status) + .build() + + private fun createEvent( + uid: String, + status: EventStatus = EventStatus.ACTIVE, + dueDate: Date = Date(), + ) = Event.builder().uid(uid) + .status(status) + .dueDate(dueDate) + .build() + private fun createTrackedEntityAttributeRepository( uid: String, unique: Boolean, diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt index 25278c3317..d83c8ea84e 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.searchTrackEntity.ui.mapper import android.content.Context import org.dhis2.R +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager @@ -19,13 +20,13 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.Calendar import java.util.Date class TEICardMapperTest { private val context: Context = mock() private val resourceManager: ResourceManager = mock() - private val currentDate = Date() private lateinit var mapper: TEICardMapper @@ -49,7 +50,7 @@ class TEICardMapperTest { @Test fun shouldReturnCardFull() { - val model = createFakeModel() + val model = createFakeModel(isOverdue = true) val result = mapper.map( searchTEIModel = model, @@ -81,7 +82,31 @@ class TEICardMapperTest { ) } - private fun createFakeModel(): SearchTeiModel { + @Test + fun shouldShowOverDueLabel() { + val overdueDate = DateUtils.getInstance().calendar + overdueDate.add(Calendar.DATE, -2) + + whenever(resourceManager.getPlural(any(), any(), any())) doReturn "2 days" + + val model = createFakeModel(overdueDate.time, true) + + val result = mapper.map( + searchTEIModel = model, + onSyncIconClick = {}, + onCardClick = {}, + onImageClick = {}, + ) + assertEquals( + result.additionalInfo[4].value, + model.overdueDate.toOverdueOrScheduledUiText(resourceManager), + ) + } + + private fun createFakeModel( + currentDate: Date = Date(), + isOverdue: Boolean = false, + ): SearchTeiModel { val attributeValues = LinkedHashMap() attributeValues["Name"] = TrackedEntityAttributeValue.builder() .value("Peter") @@ -121,7 +146,7 @@ class TEICardMapperTest { null, ) overdueDate = currentDate - isHasOverdue = true + isHasOverdue = isOverdue addEnrollment( Enrollment.builder()