diff --git a/RELEASE.md b/RELEASE.md index 19cb83d747..0cb6dcad16 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,9 +1,19 @@ -# Release notes - Android App for DHIS2 - 3.0.0.1 +# Release notes - Android App for DHIS2 - 3.0.0.2 ### Bug -[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program +[ANDROAPP-5869](https://dhis2.atlassian.net/browse/ANDROAPP-5869) IllegalArgumentException: Key "" was already used. If you are using LazyColumn/Row please make sure you provide a unique ke... -[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex +[ANDROAPP-6093](https://dhis2.atlassian.net/browse/ANDROAPP-6093) App crash when a catCombo is override -[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file +[ANDROAPP-6104](https://dhis2.atlassian.net/browse/ANDROAPP-6104) Future date can be entered in Date of birth search field + +[ANDROAPP-6124](https://dhis2.atlassian.net/browse/ANDROAPP-6124) Phone number input value type lacks of support for international symbols like \+, \(, \) + +[ANDROAPP-6225](https://dhis2.atlassian.net/browse/ANDROAPP-6225) RuntimeException: Unable to start activity ComponentInfo\{com.dhis2/org.dhis2.usescases.searchTrackEntity.SearchTEAc... + +[ANDROAPP-6271](https://dhis2.atlassian.net/browse/ANDROAPP-6271) IllegalArgumentException: The provided start date year \(2026\) is out of the years range of 2024..2024. + +[ANDROAPP-6285](https://dhis2.atlassian.net/browse/ANDROAPP-6285) Maps - App crashes when navigating from home to maps but not from analytics to maps + +[ANDROAPP-6305](https://dhis2.atlassian.net/browse/ANDROAPP-6305) Value of a TEA of valuetype AGE/DATE is not fully validated \(it is possible to add wrong dates\), and it breaks the analytics. \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt index e953a7820e..434b519908 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt @@ -25,6 +25,7 @@ import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.form.model.ValueStoreResult.ERROR_UPDATING_VALUE import org.dhis2.form.model.ValueStoreResult.VALUE_CHANGED import org.dhis2.form.model.ValueStoreResult.VALUE_HAS_NOT_CHANGED +import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.TableDataToTableModelMapper.Companion.INDICATORS_TABLE_ID import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.dataelement.DataElement @@ -109,10 +110,12 @@ class DataValuePresenter( val updatedTableModel = mapper(tableData) val updatedTables = screenState.value.tables.map { tableModel -> - if (tableModel.id == catComboUid) { - updatedTableModel.copy(overwrittenValues = tableModel.overwrittenValues) - } else { - indicatorTables() ?: tableModel + when (tableModel.id) { + catComboUid -> updatedTableModel.copy( + overwrittenValues = tableModel.overwrittenValues, + ) + INDICATORS_TABLE_ID -> indicatorTables() ?: tableModel + else -> tableModel } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt index 20bca865b6..280a6bdc93 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt @@ -8,13 +8,13 @@ import io.reactivex.Single import org.dhis2.bindings.decimalFormat import org.dhis2.commons.bindings.dataValueConflicts import org.dhis2.commons.data.tuples.Pair +import org.dhis2.commons.date.DateUtils import org.dhis2.composetable.model.TableCell import org.dhis2.data.dhislogic.AUTH_DATAVALUE_ADD import org.dhis2.data.forms.dataentry.tablefields.FieldViewModel import org.dhis2.data.forms.dataentry.tablefields.FieldViewModelFactoryImpl import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.usescases.datasets.dataSetTable.DataSetTableModel -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.UidsHelper import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope @@ -61,6 +61,7 @@ class DataValueRepository( ?.categoryComboUid() }?.distinct() } + else -> { val dataElementsSectionUid = d2.dataSetModule().sections().withDataElements() .byDataSetUid().eq(dataSetUid) @@ -157,29 +158,16 @@ class DataValueRepository( dataElement: DataElement, override: List?, ): DataElement { - return override - ?.firstOrNull { - it.dataElement().uid() == dataElement.uid() && it.categoryCombo() != null - }?.let { - DataElement.builder() - .uid(dataElement.uid()) - .code(dataElement.code()) - .name(dataElement.name()) - .displayName(dataElement.displayName()) - .shortName(dataElement.shortName()) - .displayShortName(dataElement.displayShortName()) - .description(dataElement.description()) - .displayDescription(dataElement.displayDescription()) - .valueType(dataElement.valueType()) - .zeroIsSignificant(dataElement.zeroIsSignificant()) - .aggregationType(dataElement.aggregationType()) - .formName(dataElement.formName()) - .domainType(dataElement.domainType()) - .displayFormName(dataElement.displayFormName()) - .optionSet(dataElement.optionSet()) - .categoryCombo(it.categoryCombo()).build() - } - ?: dataElement + val dataSetElement = override?.firstOrNull { + it.dataElement().uid() == dataElement.uid() && it.categoryCombo() != null + } + return if (dataSetElement != null) { + dataElement.toBuilder() + .categoryCombo(dataSetElement.categoryCombo()) + .build() + } else { + dataElement + } } private fun getDataValues(): Flowable> { @@ -290,6 +278,7 @@ class DataValueRepository( .get() .map { section -> section.greyedFields() } .toFlowable() + else -> Flowable.just(ArrayList()) } @@ -319,45 +308,38 @@ class DataValueRepository( private fun getDataElements(categoryCombo: CategoryCombo): Flowable> { return if (sectionUid != "NO_SECTION") { - val listDataElements = - d2.dataSetModule().sections().withDataElements().byDataSetUid().eq(dataSetUid) - .uid(sectionUid).blockingGet()?.dataElements() - val dataElementsOverride: MutableList = - ArrayList() - val dataSetElements = - d2.dataSetModule().dataSets().withDataSetElements().uid(dataSetUid).blockingGet() - ?.dataSetElements() - listDataElements - ?.map { transformDataElement(it, dataSetElements) } - ?.filter { it.categoryComboUid() == categoryCombo.uid() } - ?.forEach { dataElementsOverride.add(it) } + val dataElementsInSection = d2.dataSetModule().sections().withDataElements() + .byDataSetUid().eq(dataSetUid) + .uid(sectionUid) + .blockingGet() + ?.dataElements() + + val dataSetElements = d2.dataSetModule().dataSets().withDataSetElements() + .uid(dataSetUid) + .blockingGet() + ?.dataSetElements() Flowable.just( - dataElementsOverride, + dataElementsInSection + ?.map { transformDataElement(it, dataSetElements) } + ?.filter { it.categoryComboUid() == categoryCombo.uid() }, ) } else { - val dataElementUids: MutableList = - ArrayList() - val dataSetElements = - d2.dataSetModule().dataSets().withDataSetElements().byUid().eq(dataSetUid).one() - .blockingGet()?.dataSetElements() - for (dataSetElement in dataSetElements!!) { - if (dataSetElement.categoryCombo() != null && - categoryCombo.uid() == dataSetElement.categoryCombo()!!.uid() - ) { - dataElementUids.add(dataSetElement.dataElement().uid()) - } else { - val uid = d2.dataElementModule().dataElements() - .uid(dataSetElement.dataElement().uid()).blockingGet()?.categoryComboUid() - if (categoryCombo.uid() == uid) { - dataElementUids.add(dataSetElement.dataElement().uid()) - } - } - } - d2.dataElementModule().dataElements() - .byUid().`in`(dataElementUids) + val dataSetElementsInDataset = + d2.dataSetModule().dataSets().withDataSetElements() + .uid(dataSetUid) + .blockingGet() + ?.dataSetElements() + + val dataElements = d2.dataElementModule().dataElements() + .byUid().`in`(dataSetElementsInDataset?.map { it.dataElement().uid() }) .orderByName(RepositoryScope.OrderByDirection.ASC) - .get().toFlowable() + .blockingGet() + + Flowable.just( + dataElements.map { transformDataElement(it, dataSetElementsInDataset) } + .filter { it.categoryComboUid() == categoryCombo.uid() }, + ) } } @@ -403,6 +385,7 @@ class DataValueRepository( .blockingIsEmpty() hasDataValueAuthority && canWriteCatOption && canWriteOrgUnit } + else -> Flowable.just(false) } } @@ -490,35 +473,13 @@ class DataValueRepository( } fun getDataTableModel(categoryCombo: CategoryCombo): Observable { - return Flowable.zip< - List, - Map>>>, - List, - List, - List, - DataTableModel, - >( + return Flowable.zip( getDataElements(categoryCombo), getCatOptions(categoryCombo.uid()), getDataValues(), getGreyFields(), getCompulsoryDataElements(), - ) { dataElements: List, - optionsWithCategory: Map< - String, - List< - List< - Pair< - CategoryOption, - Category, - >, - >, - >, - >, - dataValues: List, - disabledDataElements: List, - compulsoryCells: List, - -> + ) { dataElements, optionsWithCategory, dataValues, disabledDataElements, compulsoryCells -> var options: List> = ArrayList() for ((_, value) in optionsWithCategory) { options = getCatOptionCombos(value, 0, ArrayList(), null) @@ -663,7 +624,7 @@ class DataValueRepository( .byDataElementUid().eq(dataElement.uid()) .byCategoryOptionComboUid().eq(categoryOptionCombo.uid()) .blockingGet() - ?.find { it.dataElement() == dataElement.uid() } + .find { it.dataElement() == dataElement.uid() } ?.syncState() val conflictInField = @@ -672,6 +633,7 @@ class DataValueRepository( State.ERROR, State.WARNING, -> true + else -> false } }?.filter { @@ -685,10 +647,13 @@ class DataValueRepository( conflictInField != null && error != null -> conflictInField + listOf(error) + valueStateSyncState == State.ERROR && conflictInField != null -> conflictInField + error != null -> listOf(error) + else -> null } @@ -696,6 +661,7 @@ class DataValueRepository( valueStateSyncState == State.WARNING && conflictInField != null -> conflictInField + else -> null } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt index e361f5fba6..6ab3f171d1 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/TableDataToTableModelMapper.kt @@ -97,10 +97,14 @@ class TableDataToTableModelMapper(val mapFieldValueToUser: MapFieldValueToUser) } return TableModel( - id = "indicators", + id = INDICATORS_TABLE_ID, title = mapFieldValueToUser.resources.getString(R.string.dashboard_indicators), tableHeaderModel = tableHeader, tableRows = tableRows, ) } + + companion object { + const val INDICATORS_TABLE_ID = "indicators" + } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index dc1a1470c7..bee7a918e3 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -44,30 +44,27 @@ class SearchRepositoryImplKt( searchParametersModel: SearchParametersModel, isOnline: Boolean, ): TrackedEntitySearchCollectionRepository { - var allowCache = false - - if (searchParametersModel != searchRepositoryJava.savedSearchParameters || !FilterManager.getInstance() - .sameFilters(searchRepositoryJava.savedFilters) - ) { - trackedEntityInstanceQuery = - searchRepositoryJava.getFilteredRepository(searchParametersModel) - } else { + trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) - allowCache = true - } - if (searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && searchParametersModel.selectedProgram == null) { - trackedEntityInstanceQuery = - trackedEntityInstanceQuery.excludeUids().`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) + val allowCache = !( + searchParametersModel != searchRepositoryJava.savedSearchParameters || + !FilterManager.getInstance().sameFilters(searchRepositoryJava.savedFilters) + ) + + if ( + searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && + searchParametersModel.selectedProgram == null + ) { + trackedEntityInstanceQuery = trackedEntityInstanceQuery.excludeUids() + .`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) } - val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { + return if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst() } else { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() } - - return pagerFlow } override suspend fun searchParameters( diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java index 579cd99d6a..d9d43b6f68 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java @@ -363,7 +363,8 @@ private void initSearchParameters() { uid, selectedOrgUnit, ValueType.ORGANISATION_UNIT, - null + null, + true ) ); return Unit.INSTANCE; @@ -397,7 +398,6 @@ private void configureBottomNavigation() { this, () -> { presenter.trackSearchMapVisualization(); - viewModel.setMapScreen(); showMap(); showSearchAndFilterButtons(); return null; diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt index bd8ac8e108..727b704650 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt @@ -89,6 +89,7 @@ class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { (context as SearchTEActivity).searchComponent.plus( SearchTEMapModule(), ).inject(this) + viewModel.setMapScreen() } override fun onCreateView( diff --git a/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt b/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt index f396e5c2fd..ee876e27ec 100644 --- a/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenterTest.kt @@ -18,6 +18,7 @@ import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.form.model.StoreResult import org.dhis2.form.model.ValueStoreResult +import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.TableDataToTableModelMapper.Companion.INDICATORS_TABLE_ID import org.hisp.dhis.android.core.category.CategoryCombo import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.dataelement.DataElement @@ -381,7 +382,7 @@ class DataValuePresenterTest { } val mockedIndicatorTableModel = mock { - on { id } doReturn null + on { id } doReturn INDICATORS_TABLE_ID } val mockedUpdatedTableModel = mock { @@ -426,6 +427,72 @@ class DataValuePresenterTest { verify(view).onValueProcessed() } + @Test + fun shouldUpdateValueWhenSavedForTwoTablesAndIndicators() { + val mockedTableCell = mock { + on { id } doReturn "mocked_id" + on { column } doReturn 1 + on { row } doReturn 0 + on { value } doReturn "valueToSave" + } + + val mockedTableModelA = mock { + on { id } doReturn "tableIdA" + on { hasCellWithId(any()) } doReturn true + } + + val mockedTableModelB = mock { + on { id } doReturn "tableIdB" + on { hasCellWithId(any()) } doReturn true + } + + val mockedIndicatorTableModel = mock { + on { id } doReturn INDICATORS_TABLE_ID + } + + val mockedUpdatedTableModel = mock { + on { id } doReturn "tableIdA_updated" + on { hasCellWithId(any()) } doReturn true + } + + val mockedUpdatedIndicatorTableModel = mock { + on { id } doReturn "updated_indicator" + } + + val tableStateValue = presenter.mutableTableData() + tableStateValue.value = TableScreenState( + listOf(mockedTableModelA, mockedTableModelB, mockedIndicatorTableModel), + ) + + whenever(valueStore.save(any(), any(), any(), any(), any(), any())) doReturn Flowable.just( + StoreResult( + uid = "id", + valueStoreResult = ValueStoreResult.VALUE_CHANGED, + valueStoreResultMessage = null, + ), + ) + + whenever(dataValueRepository.getDataTableModel(any())) doReturn Observable.just( + mockedDataTableModel, + ) + whenever(dataValueRepository.setTableData(any(), any())) doReturn mockedTableData + whenever(mapper.invoke(any())) doReturn mockedUpdatedTableModel + whenever( + mockedUpdatedTableModel.copy(overwrittenValues = mockedTableModel.overwrittenValues), + ) doReturn mockedUpdatedTableModel + + whenever(dataValueRepository.getDataSetIndicators()) doReturn Single.just(mockedIndicators) + whenever(mapper.map(any())) doReturn mockedUpdatedIndicatorTableModel + + presenter.onSaveValueChange(mockedTableCell) + + assertTrue(presenter.currentState().value.tables.size == 3) + assertTrue(presenter.currentState().value.tables[0].id == "tableIdA_updated") + assertTrue(presenter.currentState().value.tables[1].id == "tableIdB") + assertTrue(presenter.currentState().value.tables.last().id == "updated_indicator") + verify(view).onValueProcessed() + } + @Test fun shouldSetErrorValue() { val mockedTableCell = mock { diff --git a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index 70f5d4a4cf..faae1a08ed 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java +++ b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.hisp.dhis.android.core.dataset.DataInputPeriod; import org.hisp.dhis.android.core.event.EventStatus; import org.hisp.dhis.android.core.period.PeriodType; @@ -26,6 +27,7 @@ public static DateUtils getInstance() { } public static final String DATABASE_FORMAT_EXPRESSION = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + public static final String DATABASE_FORMAT_NO_TIME = "yyyy-MM-dd"; public static final String DATABASE_FORMAT_EXPRESSION_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ss"; public static final String DATABASE_FORMAT_EXPRESSION_NO_SECONDS = "yyyy-MM-dd'T'HH:mm"; public static final String DATE_TIME_FORMAT_EXPRESSION = "yyyy-MM-dd HH:mm"; @@ -769,4 +771,22 @@ public static int[] getDifference(Date startDate, Date endDate) { org.joda.time.Period interval = new org.joda.time.Period(startDate.getTime(), endDate.getTime(), org.joda.time.PeriodType.yearMonthDayTime()); return new int[]{interval.getYears(), interval.getMonths(), interval.getDays()}; } + + public Boolean isDataSetExpired(int expiredDays, Date periodInitialDate) { + return Calendar.getInstance().getTime().getTime() > periodInitialDate.getTime() + TimeUnit.DAYS.toMillis(expiredDays); + } + + public Boolean isInsideInputPeriod(DataInputPeriod dataInputPeriodModel) { + if (dataInputPeriodModel.openingDate() == null && dataInputPeriodModel.closingDate() != null) + return Calendar.getInstance().getTime().getTime() < dataInputPeriodModel.closingDate().getTime(); + + if (dataInputPeriodModel.openingDate() != null && dataInputPeriodModel.closingDate() == null) + return dataInputPeriodModel.openingDate().getTime() < Calendar.getInstance().getTime().getTime(); + + if (dataInputPeriodModel.openingDate() == null && dataInputPeriodModel.closingDate() == null) + return true; + + return dataInputPeriodModel.openingDate().getTime() < Calendar.getInstance().getTime().getTime() + && Calendar.getInstance().getTime().getTime() < dataInputPeriodModel.closingDate().getTime(); + } } diff --git a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt index 63bf190731..4536537ad5 100644 --- a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt +++ b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeFragment.kt @@ -27,7 +27,7 @@ const val ARG_SINGLE_SELECTION = "OUTreeFragment.ARG_SINGLE_SELECTION" const val ARG_SCOPE = "OUTreeFragment.ARG_SCOPE" const val ARG_PRE_SELECTED_OU = "OUTreeFragment.ARG_PRE_SELECTED_OU" -class OUTreeFragment private constructor() : DialogFragment() { +class OUTreeFragment : DialogFragment() { class Builder { private var showAsDialog = false diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt index 4f77acb803..135ba01ff7 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.R @@ -39,6 +40,8 @@ import org.hisp.dhis.android.core.common.valuetype.validation.failures.DateFailu import org.hisp.dhis.android.core.common.valuetype.validation.failures.DateTimeFailure import org.hisp.dhis.android.core.common.valuetype.validation.failures.TimeFailure import timber.log.Timber +import java.text.ParseException +import java.text.SimpleDateFormat import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -398,6 +401,7 @@ class FormViewModel( intent.valueType, intent.value, intent.fieldMask, + intent.allowFutureDates, ) createRowAction( @@ -552,10 +556,10 @@ class FormViewModel( } val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") - + val isValidDateFormat = isValidDate(dateTimeString.substring(0, 10)) try { val date = LocalDateTime.parse(dateTimeString, formatter) - if (allowFutureDates == false && date.isAfter(LocalDateTime.now())) { + if (allowFutureDates == false && date.isAfter(LocalDateTime.now()) || !isValidDateFormat) { return Result.Failure(DateFailure.ParseException) } return valueType.validator.validate(dateTimeString) @@ -585,8 +589,9 @@ class FormViewModel( try { val date = LocalDate.parse(dateString, formatter) - if (allowFutureDates == false && date.isAfter(LocalDate.now())) { - return Result.Failure(DateFailure.ParseException) + val formatDateValid = isValidDate(dateString) + if (allowFutureDates == false && date.isAfter(LocalDate.now()) || !formatDateValid) { + return Result.Failure(Throwable()) } return valueType.validator.validate(dateString) } catch (e: DateTimeParseException) { @@ -594,6 +599,17 @@ class FormViewModel( } } + private fun isValidDate(text: String): Boolean { + val format = SimpleDateFormat(DateUtils.DATABASE_FORMAT_NO_TIME) + format.isLenient = false + return try { + format.parse(text) + true + } catch (e: ParseException) { + false + } + } + private fun createRowAction( uid: String, value: String?, diff --git a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt index b6a7863825..1f37d3b459 100644 --- a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt +++ b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt @@ -24,6 +24,7 @@ sealed class FormIntent { val value: String?, val valueType: ValueType?, val fieldMask: String? = null, + val allowFutureDates: Boolean? = false, ) : FormIntent() data class OnQrCodeScanned( @@ -42,7 +43,6 @@ sealed class FormIntent { val uid: String, val value: String?, val valueType: ValueType?, - val allowFutureDates: Boolean? = false, ) : FormIntent() data class ClearValue( diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt index 78d23930f2..a013ab1125 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt @@ -102,13 +102,17 @@ fun ProvideInputAge( } is AgeInputType.DateOfBirth -> { - saveValue( - intentHandler, - fieldUiModel.uid, - formatUIDateToStored(type.value.text), - fieldUiModel.valueType, - fieldUiModel.allowFutureDates, - ) + formatUIDateToStored(type.value.text) + .takeIf { it != fieldUiModel.value } + ?.let { + saveValue( + intentHandler, + fieldUiModel.uid, + it, + fieldUiModel.valueType, + fieldUiModel.allowFutureDates, + ) + } } AgeInputType.None -> { @@ -135,14 +139,23 @@ private fun saveValue( valueType: ValueType?, allowFutureDates: Boolean?, ) { - intentHandler.invoke( - FormIntent.OnTextChange( - uid, - value, - valueType, - allowFutureDates ?: false, - ), - ) + when (value?.length) { + null, 10 -> intentHandler.invoke( + FormIntent.OnSave( + uid, + value, + valueType, + allowFutureDates = allowFutureDates, + ), + ) + else -> intentHandler.invoke( + FormIntent.OnTextChange( + uid, + value, + valueType, + ), + ) + } } private fun formatStoredDateToUI(inputDateString: String): String { diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt index 8301f6d1f4..48a9d8c7d4 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt @@ -69,14 +69,21 @@ fun ProvideInputDate( onNextClicked = onNextClicked, onValueChanged = { value = it ?: TextFieldValue() - intentHandler.invoke( + val formIntent = if (value.text.length == 8) { + FormIntent.OnSave( + uid = fieldUiModel.uid, + value = formatUIDateToStored(it?.text, fieldUiModel.valueType), + valueType = fieldUiModel.valueType, + allowFutureDates = fieldUiModel.allowFutureDates, + ) + } else { FormIntent.OnTextChange( uid = fieldUiModel.uid, value = formatUIDateToStored(it?.text, fieldUiModel.valueType), valueType = fieldUiModel.valueType, - allowFutureDates = fieldUiModel.allowFutureDates ?: true, - ), - ) + ) + } + intentHandler.invoke(formIntent) }, selectableDates = selectableDates, yearRange = yearIntRange, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index feffbf2e03..8a80479a17 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,15 +2,15 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" -vCode = "134" -vName = "3.0.0.1" +vCode = "135" +vName = "3.0.0.2" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.2" kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "0.2" +designSystem = "0.2.1" dhis2sdk = "1.10.0.1" ruleEngine = "3.0.0" expressionParser = "1.1.0" diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 19cb83d747..e65b81ff8c 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,9 +1,8 @@ -# Release notes - Android App for DHIS2 - 3.0.0.1 - -### Bug - -[ANDROAPP-6194](https://dhis2.atlassian.net/browse/ANDROAPP-6194) Unable to search outside the program - -[ANDROAPP-6195](https://dhis2.atlassian.net/browse/ANDROAPP-6195) Missing terms in transifex - -[ANDROAPP-6210](https://dhis2.atlassian.net/browse/ANDROAPP-6210) UninitializedPropertyAccessException on breaking the glass \ No newline at end of file +# Release notes - Android App for DHIS2 - 3.0.0.2 + +- IllegalArgumentException: Key "" was already used. +- App crash when a catCombo is override +- Future date can be entered in Date of birth search field +- Phone number input value type lacks of support for international symbols like \+, \(, \) +- Maps - App crashes when navigating from home to maps but not from analytics to maps +- Value of a TEA of valuetype AGE/DATE is not fully validated \ No newline at end of file