From 6c9fe0b301523f14511cdddc82e485dde3250b72 Mon Sep 17 00:00:00 2001 From: danielTari Date: Tue, 15 Sep 2026 09:37:40 +0200 Subject: [PATCH 1/3] add attributeOptionCombo parameter to dataValueSets requests remove paging=false fix default future periods number --- .../hisp/dhis/android/core/dataset/DataSet.kt | 5 +++ .../core/datavalue/internal/DataValueCall.kt | 35 +++++++++++++++++-- .../internal/DataValueNetworkHandler.kt | 6 +++- .../data/internal/AggregatedDataCall.kt | 3 +- .../AggregatedDataCallBundleFactory.kt | 3 +- .../datavalue/DataValueNetworkHandlerImpl.kt | 5 ++- .../network/datavalue/DataValueService.kt | 4 +-- 7 files changed, 53 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.kt b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.kt index fb5718a471f..5eec309f7bc 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/dataset/DataSet.kt @@ -110,6 +110,11 @@ data class DataSet( class Builder : DataSetBuilder() companion object { + internal const val DEFAULT_FUTURE_PERIODS = 1 + + internal fun futurePeriodsOrDefault(dataSet: DataSet): Int = + dataSet.openFuturePeriods() ?: DEFAULT_FUTURE_PERIODS + @JvmStatic fun builder(): Builder = Builder() .access(defaultAccess()) diff --git a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCall.kt b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCall.kt index adf91787c85..3629b996f14 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCall.kt @@ -29,7 +29,11 @@ package org.hisp.dhis.android.core.datavalue.internal import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader import org.hisp.dhis.android.core.arch.call.factories.internal.QueryCall +import org.hisp.dhis.android.core.arch.helpers.internal.UrlLengthHelper +import org.hisp.dhis.android.core.category.internal.CategoryOptionComboStore +import org.hisp.dhis.android.core.dataset.DataSet import org.hisp.dhis.android.core.datavalue.DataValue +import org.hisp.dhis.android.core.domain.aggregated.data.internal.AggregatedDataCallBundle import org.koin.core.annotation.Singleton @Singleton @@ -37,13 +41,40 @@ internal class DataValueCall( private val networkHandler: DataValueNetworkHandler, private val handler: DataValueHandler, private val apiDownloader: APIDownloader, + private val categoryOptionComboStore: CategoryOptionComboStore, ) : QueryCall { + companion object { + private const val QUERY_WITHOUT_UIDS_LENGTH = ( + "dataValueSets?fields=dataElement,period,orgUnit,categoryOptionCombo,attributeOptionCombo,value," + + "storedBy,created,lastUpdated,comment,followup,deleted&lastUpdated=0000-00-00T00:00:00.000" + + "&dataSet=&period=&orgUnit=&attributeOptionCombo=&children=true&includeDeleted=true" + ).length + + private const val DATA_SET_UIDS = 1 + } + override suspend fun download(query: DataValueQuery): List { - return query.bundle.dataSets.mapNotNull { it.uid() }.flatMap { dataSetUid -> + val bundle = query.bundle + val availableUids = availableUidsForAttributeOptionCombos(bundle) + + return bundle.dataSets.flatMap { dataSet -> + val attributeOptionComboUids = attributeOptionComboUids(dataSet, availableUids) apiDownloader.downloadListAsCoroutine(handler) { - networkHandler.getDataValuesForDataSet(dataSetUid, query.bundle) + networkHandler.getDataValuesForDataSet(dataSet.uid(), attributeOptionComboUids, bundle) } } } + + private suspend fun attributeOptionComboUids(dataSet: DataSet, availableUids: Int): List { + val uids = categoryOptionComboStore.getForCategoryCombo(dataSet.categoryCombo().uid()) + .map { it.uid() } + + return if (uids.size <= availableUids) uids else emptyList() + } + + private fun availableUidsForAttributeOptionCombos(bundle: AggregatedDataCallBundle): Int { + return UrlLengthHelper.getHowManyUidsFitInURL(QUERY_WITHOUT_UIDS_LENGTH) - + DATA_SET_UIDS - bundle.periodIds.size - bundle.rootOrganisationUnitUids.size + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueNetworkHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueNetworkHandler.kt index 02d19c14d55..abcaa107ee0 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueNetworkHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/datavalue/internal/DataValueNetworkHandler.kt @@ -36,7 +36,11 @@ import org.hisp.dhis.android.core.imports.internal.DataValueImportSummaryWebResp import org.hisp.dhis.android.core.maintenance.D2Error internal interface DataValueNetworkHandler { - suspend fun getDataValuesForDataSet(dataSetUid: String, bundle: AggregatedDataCallBundle): List + suspend fun getDataValuesForDataSet( + dataSetUid: String, + attributeOptionComboUids: List, + bundle: AggregatedDataCallBundle, + ): List suspend fun postDataValues(dataValueSet: DataValueSet): Result suspend fun postDataValuesWebResponse( dataValueSet: DataValueSet, diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCall.kt b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCall.kt index f97192ea627..9919acc3e06 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCall.kt @@ -37,6 +37,7 @@ import org.hisp.dhis.android.core.dataapproval.DataApproval import org.hisp.dhis.android.core.dataapproval.internal.DataApprovalCall import org.hisp.dhis.android.core.dataapproval.internal.DataApprovalQuery import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSet.Companion.futurePeriodsOrDefault import org.hisp.dhis.android.core.dataset.DataSetCompleteRegistration import org.hisp.dhis.android.core.dataset.internal.DataSetCompleteRegistrationCall import org.hisp.dhis.android.core.dataset.internal.DataSetCompleteRegistrationQuery @@ -143,7 +144,7 @@ internal class AggregatedDataCall( .dataSet(dataSet.uid()) .periodType(dataSet.periodType()!!) .pastPeriods(bundle.key.pastPeriods) - .futurePeriods(dataSet.openFuturePeriods() ?: 0) + .futurePeriods(futurePeriodsOrDefault(dataSet)) .dataElementsHash(hashHelper.getDataSetDataElementsHash(dataSet)) .organisationUnitsHash(bundle.allOrganisationUnitUidsSet.hashCode()) .lastUpdated(syncDate) diff --git a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCallBundleFactory.kt b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCallBundleFactory.kt index 74e751b39b4..c6a22aeae89 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCallBundleFactory.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataCallBundleFactory.kt @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.domain.aggregated.data.internal import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSet.Companion.futurePeriodsOrDefault import org.hisp.dhis.android.core.dataset.DataSetCollectionRepository import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.organisationunit.OrganisationUnitCollectionRepository @@ -107,7 +108,7 @@ internal class AggregatedDataCallBundleFactory( organisationUnitsHash: Int, ): AggregatedDataCallBundleKey { val pastPeriods = getPastPeriods(dataSetSettings, dataSet) - val futurePeriods = if (dataSet.openFuturePeriods() == null) 1 else dataSet.openFuturePeriods()!! + val futurePeriods = futurePeriodsOrDefault(dataSet) val syncValue = syncValues[dataSet.uid()] return AggregatedDataCallBundleKey( diff --git a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt index c36c016f26c..dd0f183a2b6 100644 --- a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt @@ -51,6 +51,7 @@ internal class DataValueNetworkHandlerImpl( override suspend fun getDataValuesForDataSet( dataSetUid: String, + attributeOptionComboUids: List, bundle: AggregatedDataCallBundle, ): List { val apiResponse = service.getDataValues( @@ -59,8 +60,10 @@ internal class DataValueNetworkHandlerImpl( dataSetUids = dataSetUid, periodIds = commaSeparatedCollectionValues(bundle.periodIds), orgUnitUids = commaSeparatedCollectionValues(bundle.rootOrganisationUnitUids), + attributeOptionComboUids = attributeOptionComboUids + .takeIf { it.isNotEmpty() } + ?.let { commaSeparatedCollectionValues(it) }, children = true, - paging = false, includeDeleted = true, ) return apiResponse.dataValues.map { it.toDomain(dataSetUid) } diff --git a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueService.kt b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueService.kt index dfbaf8d4d02..57eea454117 100644 --- a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueService.kt +++ b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueService.kt @@ -40,8 +40,8 @@ internal class DataValueService(private val client: HttpServiceClient) { dataSetUids: String, periodIds: String, orgUnitUids: String, + attributeOptionComboUids: String?, children: Boolean, - paging: Boolean, includeDeleted: Boolean?, ): DataValueSetDTO { return client.get { @@ -52,9 +52,9 @@ internal class DataValueService(private val client: HttpServiceClient) { attribute("dataSet", dataSetUids) attribute("period", periodIds) attribute("orgUnit", orgUnitUids) + attribute("attributeOptionCombo", attributeOptionComboUids) attribute("children", children) attribute("includeDeleted", includeDeleted) - paging(paging) } } } From 2fd946df23b4acec053144995957ee7d9165629c Mon Sep 17 00:00:00 2001 From: danielTari Date: Tue, 15 Sep 2026 09:38:10 +0200 Subject: [PATCH 2/3] update and add tests --- ...aValueEndpointCallRealIntegrationShould.kt | 2 +- .../datavalue/internal/DataValueCallShould.kt | 160 ++++++++++++++++++ ...atedDataSyncLastUpdatedCalculatorShould.kt | 23 ++- 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCallShould.kt diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValueEndpointCallRealIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValueEndpointCallRealIntegrationShould.kt index 14d9b572f39..79d273cb48e 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValueEndpointCallRealIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/datavalue/internal/DataValueEndpointCallRealIntegrationShould.kt @@ -85,7 +85,7 @@ class DataValueEndpointCallRealIntegrationShould : BaseRealIntegrationTest() { val apiDownloader: APIDownloader = APIDownloaderImpl(resourceHandler) val dataValueNetworkHandler = DataValueNetworkHandlerImpl(d2.httpServiceClient(), d2.coroutineAPICallExecutor()) - return DataValueCall(dataValueNetworkHandler, dataValueHandler, apiDownloader) + return DataValueCall(dataValueNetworkHandler, dataValueHandler, apiDownloader, koin.get()) .download(DataValueQuery(bundle)) } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCallShould.kt b/core/src/test/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCallShould.kt new file mode 100644 index 00000000000..579e6a4aa99 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/datavalue/internal/DataValueCallShould.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2004-2025, 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.datavalue.internal + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.hisp.dhis.android.core.arch.api.executors.internal.APIDownloader +import org.hisp.dhis.android.core.arch.handlers.internal.Handler +import org.hisp.dhis.android.core.category.CategoryOptionCombo +import org.hisp.dhis.android.core.category.internal.CategoryOptionComboStore +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.datavalue.DataValue +import org.hisp.dhis.android.core.domain.aggregated.data.internal.AggregatedDataCallBundle +import org.hisp.dhis.android.core.domain.aggregated.data.internal.AggregatedDataCallBundleKey +import org.hisp.dhis.android.core.period.PeriodType +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doSuspendableAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.verifyBlocking +import org.mockito.kotlin.whenever + +@RunWith(JUnit4::class) +class DataValueCallShould { + + private val networkHandler: DataValueNetworkHandler = mock() + private val handler: DataValueHandler = mock() + private val apiDownloader: APIDownloader = mock() + private val categoryOptionComboStore: CategoryOptionComboStore = mock() + + private val dataSet1: DataSet = mock() + private val dataSet2: DataSet = mock() + + private val dataSetCall: DataValueCall by lazy { + DataValueCall(networkHandler, handler, apiDownloader, categoryOptionComboStore) + } + + @Before + fun setUp() { + whenever(dataSet1.uid()) doReturn DS1 + whenever(dataSet1.categoryCombo()) doReturn ObjectWithUid.create(CC1) + whenever(dataSet2.uid()) doReturn DS2 + whenever(dataSet2.categoryCombo()) doReturn ObjectWithUid.create(CC2) + + apiDownloader.stub { + onBlocking { downloadListAsCoroutine(any>(), any()) } doSuspendableAnswer { + it.getArgument List>(1).invoke() + } + } + networkHandler.stub { + onBlocking { getDataValuesForDataSet(any(), any(), any()) } doReturn emptyList() + } + } + + @Test + fun send_the_attribute_option_combos_of_the_data_set() = runTest { + whenever(categoryOptionComboStore.getForCategoryCombo(CC1)) doReturn categoryOptionCombos(AOC1, AOC2) + + dataSetCall.download(DataValueQuery(bundle(listOf(dataSet1)))) + + verifyBlocking(networkHandler) { + getDataValuesForDataSet(eq(DS1), eq(listOf(AOC1, AOC2)), any()) + } + } + + @Test + fun send_one_request_per_data_set_with_its_own_attribute_option_combos() = runTest { + whenever(categoryOptionComboStore.getForCategoryCombo(CC1)) doReturn categoryOptionCombos(AOC1) + whenever(categoryOptionComboStore.getForCategoryCombo(CC2)) doReturn categoryOptionCombos(AOC2) + + dataSetCall.download(DataValueQuery(bundle(listOf(dataSet1, dataSet2)))) + + verifyBlocking(networkHandler) { getDataValuesForDataSet(eq(DS1), eq(listOf(AOC1)), any()) } + verifyBlocking(networkHandler) { getDataValuesForDataSet(eq(DS2), eq(listOf(AOC2)), any()) } + } + + @Test + fun omit_the_attribute_option_combos_if_they_do_not_fit_in_the_url() = runTest { + whenever(categoryOptionComboStore.getForCategoryCombo(CC1)) doReturn categoryOptionCombos(AOC1, AOC2) + val periodIds = (1..500).map { "20200$it" } + + dataSetCall.download(DataValueQuery(bundle(listOf(dataSet1), periodIds))) + + verifyBlocking(networkHandler) { getDataValuesForDataSet(eq(DS1), eq(emptyList()), any()) } + } + + @Test + fun return_the_data_values_of_every_data_set() = runTest { + whenever(categoryOptionComboStore.getForCategoryCombo(any())) doReturn emptyList() + whenever(networkHandler.getDataValuesForDataSet(eq(DS1), any(), any())) doReturn listOf(dataValue("de1")) + whenever(networkHandler.getDataValuesForDataSet(eq(DS2), any(), any())) doReturn listOf(dataValue("de2")) + + val dataValues = dataSetCall.download(DataValueQuery(bundle(listOf(dataSet1, dataSet2)))) + + assertThat(dataValues.map { it.dataElement() }).containsExactly("de1", "de2") + } + + private fun bundle(dataSets: List, periodIds: List = listOf("202001")) = + AggregatedDataCallBundle( + key = AggregatedDataCallBundleKey(PeriodType.Monthly, 1, 1, null), + dataSets = dataSets, + periodIds = periodIds, + rootOrganisationUnitUids = listOf(OU1), + allOrganisationUnitUidsSet = setOf(OU1), + ) + + private fun categoryOptionCombos(vararg uids: String): List = + uids.map { CategoryOptionCombo.builder().uid(it).build() } + + private fun dataValue(dataElementUid: String): DataValue = + DataValue.builder() + .dataElement(dataElementUid) + .period("202001") + .organisationUnit(OU1) + .categoryOptionCombo("categoryOptCom1") + .attributeOptionCombo(AOC1) + .build() + + companion object { + private const val DS1 = "dataSet1" + private const val DS2 = "dataSet2" + private const val CC1 = "categoryCombo1" + private const val CC2 = "categoryCombo2" + private const val AOC1 = "attributeOptCom1" + private const val AOC2 = "attributeOptCom2" + private const val OU1 = "organisationUni1" + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSyncLastUpdatedCalculatorShould.kt b/core/src/test/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSyncLastUpdatedCalculatorShould.kt index f0dadbba1bd..bf7e9c2ad0b 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSyncLastUpdatedCalculatorShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/domain/aggregated/data/internal/AggregatedDataSyncLastUpdatedCalculatorShould.kt @@ -30,6 +30,7 @@ package org.hisp.dhis.android.core.domain.aggregated.data.internal import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.arch.helpers.DateUtils import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSet.Companion.futurePeriodsOrDefault import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -134,7 +135,27 @@ class AggregatedDataSyncLastUpdatedCalculatorShould { dataSet.openFuturePeriods()!!, organisationUnitsHash, ) - assertThat(lastUpdated).isNull() + assertThat(lastUpdated).isNull() + } + + @Test + fun return_expected_last_updated_if_data_set_has_no_open_future_periods() { + val dataSetWithoutOpenFuturePeriods = dataSet.toBuilder().openFuturePeriods(null).build() + whenever(hashHelper.getDataSetDataElementsHash(dataSetWithoutOpenFuturePeriods)) + .thenReturn(dataElementsHash) + + val syncValueWithoutOpenFuturePeriods = syncValue.toBuilder() + .futurePeriods(futurePeriodsOrDefault(dataSetWithoutOpenFuturePeriods)) + .build() + + val lastUpdated = calculator.getLastUpdated( + syncValueWithoutOpenFuturePeriods, + dataSetWithoutOpenFuturePeriods, + pastPeriods, + futurePeriodsOrDefault(dataSetWithoutOpenFuturePeriods), + organisationUnitsHash, + ) + assertThat(lastUpdated).isEqualTo(expectedLastUpdated) } @Test From 68f1c5ac127e5fdbca537557ddba476bd20aff04 Mon Sep 17 00:00:00 2001 From: danielTari Date: Tue, 15 Sep 2026 18:46:44 +0200 Subject: [PATCH 3/3] apply the dataValueSet properties as defaults for its dataValues inherit dataValueSet properties in dataValues missing them --- .../android/network/datavalue/DataValueDTO.kt | 18 +++++++++--------- .../datavalue/DataValueNetworkHandlerImpl.kt | 2 +- .../network/datavalue/DataValueSetDTO.kt | 14 ++++++++++++-- .../android/core/datavalue/DataValueShould.kt | 3 ++- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueDTO.kt b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueDTO.kt index 5e2c13089d9..f191da3c23a 100644 --- a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueDTO.kt +++ b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueDTO.kt @@ -37,10 +37,10 @@ import org.hisp.dhis.android.network.common.dto.BaseDeletableDataObjectDTO internal data class DataValueDTO( override val deleted: Boolean?, val dataElement: String, - val period: String, - val orgUnit: String, - val categoryOptionCombo: String, - val attributeOptionCombo: String, + val period: String? = null, + val orgUnit: String? = null, + val categoryOptionCombo: String? = null, + val attributeOptionCombo: String? = null, val value: String?, val storedBy: String?, val created: String?, @@ -50,14 +50,14 @@ internal data class DataValueDTO( ) : BaseDeletableDataObjectDTO { @Suppress("ComplexMethod") - fun toDomain(sourceDataSet: String?): DataValue { + fun toDomain(sourceDataSet: String?, dataValueSet: DataValueSetDTO): DataValue { val builder = DataValue.builder().apply { deleted?.let { deleted(it) } dataElement(dataElement) - period(period) - organisationUnit(orgUnit) - categoryOptionCombo(categoryOptionCombo) - attributeOptionCombo(attributeOptionCombo) + (period ?: dataValueSet.period)?.let { period(it) } + (orgUnit ?: dataValueSet.orgUnit)?.let { organisationUnit(it) } + (categoryOptionCombo ?: dataValueSet.categoryOptionCombo)?.let { categoryOptionCombo(it) } + (attributeOptionCombo ?: dataValueSet.attributeOptionCombo)?.let { attributeOptionCombo(it) } value?.let { value(it) } storedBy?.let { storedBy(it) } created?.let { created(DateUtils.DATE_FORMAT.parse(it)) } diff --git a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt index dd0f183a2b6..9e0564257b2 100644 --- a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt +++ b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueNetworkHandlerImpl.kt @@ -66,7 +66,7 @@ internal class DataValueNetworkHandlerImpl( children = true, includeDeleted = true, ) - return apiResponse.dataValues.map { it.toDomain(dataSetUid) } + return apiResponse.dataValues.map { it.toDomain(dataSetUid, apiResponse) } } override suspend fun postDataValues(dataValueSet: DataValueSet): Result { diff --git a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueSetDTO.kt b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueSetDTO.kt index 7813d1bd1e3..e67eaa82469 100644 --- a/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueSetDTO.kt +++ b/core/src/main/java/org/hisp/dhis/android/network/datavalue/DataValueSetDTO.kt @@ -31,14 +31,24 @@ package org.hisp.dhis.android.network.datavalue import kotlinx.serialization.Serializable import org.hisp.dhis.android.core.datavalue.internal.DataValueSet +/** + * The properties of a dataValueSet act as defaults for the dataValues it contains: whenever a value + * is constant across the whole response, the server may report it here and omit it from every + * dataValue. DHIS2 2.44 does it with the attributeOptionCombo as soon as the request filters by it. + */ @Serializable internal data class DataValueSetDTO( - val dataSet: String?, + val dataSet: String? = null, + val period: String? = null, + val orgUnit: String? = null, + val categoryOptionCombo: String? = null, + val attributeOptionCombo: String? = null, + val completeDate: String? = null, val dataValues: List = emptyList(), ) { fun toDomain(): DataValueSet { return DataValueSet( - dataValues = dataValues.map { it.toDomain(dataSet) }, + dataValues = dataValues.map { it.toDomain(dataSet, this) }, dataSet = dataSet, ) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/datavalue/DataValueShould.kt b/core/src/test/java/org/hisp/dhis/android/core/datavalue/DataValueShould.kt index c48aedd7242..1f50436d572 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/datavalue/DataValueShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/datavalue/DataValueShould.kt @@ -31,6 +31,7 @@ import com.google.common.truth.Truth.assertThat import org.hisp.dhis.android.core.common.BaseIdentifiableObject import org.hisp.dhis.android.core.common.CoreObjectShould import org.hisp.dhis.android.network.datavalue.DataValueDTO +import org.hisp.dhis.android.network.datavalue.DataValueSetDTO import org.junit.Test internal class DataValueShould : CoreObjectShould( @@ -41,7 +42,7 @@ internal class DataValueShould : CoreObjectShould( @Test override fun map_from_json_string() { val dataValueDTO = deserialize() - val dataValue = dataValueDTO.toDomain("lyLU2wR22tC") + val dataValue = dataValueDTO.toDomain("lyLU2wR22tC", DataValueSetDTO()) assertThat(dataValue.dataElement()).isEqualTo("s46m5MS0hxu") assertThat(dataValue.period()).isEqualTo("201712")