diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt index d68bc42885a..f228695874c 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt @@ -19,8 +19,10 @@ import android.widget.Button import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.lifecycle.Observer import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import org.odk.collect.androidshared.data.Consumable /** * Convenience wrapper around Android's [Snackbar] API. @@ -40,6 +42,11 @@ object SnackbarUtils { return showSnackbar(parentView, message, 3500, anchorView, action, displayDismissButton) } + @JvmStatic + fun showLongSnackbar(parentView: View, snackbarDetails: SnackbarDetails) { + showLongSnackbar(parentView, snackbarDetails.text, action = snackbarDetails.action) + } + @JvmStatic @JvmOverloads fun showLongSnackbar( @@ -74,7 +81,8 @@ object SnackbarUtils { lastSnackbar?.dismiss() lastSnackbar = Snackbar.make(parentView, message.trim(), duration).apply { - val textView = view.findViewById(com.google.android.material.R.id.snackbar_text) + val textView = + view.findViewById(com.google.android.material.R.id.snackbar_text) textView.isSingleLine = false if (anchorView?.visibility != View.GONE) { @@ -88,7 +96,8 @@ object SnackbarUtils { setOnClickListener { dismiss() } - contentDescription = context.getString(org.odk.collect.strings.R.string.close_snackbar) + contentDescription = + context.getString(org.odk.collect.strings.R.string.close_snackbar) } val params = LinearLayout.LayoutParams( @@ -116,8 +125,23 @@ object SnackbarUtils { lastSnackbar?.show() } - data class Action( + data class SnackbarDetails @JvmOverloads constructor( val text: String, - val listener: () -> Unit + val action: Action? = null ) + + data class Action(val text: String, val listener: () -> Unit) + + abstract class SnackbarPresenterObserver(private val parentView: View) : + Observer> { + + abstract fun getSnackbarDetails(value: T): SnackbarDetails + + override fun onChanged(consumable: Consumable) { + if (!consumable.isConsumed()) { + showLongSnackbar(parentView, getSnackbarDetails(consumable.value)) + consumable.consume() + } + } + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index e35e53feb60..7f4b7ca734d 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -1,12 +1,18 @@ package org.odk.collect.android.feature.formmanagement import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith +import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.AccessControlPage +import org.odk.collect.android.support.pages.EditSavedFormPage import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer import org.odk.collect.android.support.pages.MainMenuPage +import org.odk.collect.android.support.pages.ProjectSettingsPage import org.odk.collect.android.support.pages.SaveOrDiscardFormDialog import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain @@ -16,23 +22,24 @@ import org.odk.collect.strings.R.string @RunWith(AndroidJUnit4::class) class BulkFinalizationTest { - val rule = CollectTestRule() + val testDependencies = TestDependencies() + val rule = CollectTestRule(useDemoProject = false) @get:Rule - val chain: RuleChain = TestRuleChain.chain().around(rule) + val chain: RuleChain = TestRuleChain.chain(testDependencies).around(rule) @Test fun canBulkFinalizeDrafts() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .startBlankForm("One Question") .fillOutAndSave(QuestionAndAnswer("what is your age", "98")) .clickDrafts(2) - .clickOptionsIcon(string.finalize_all_drafts) - .clickOnString(string.finalize_all_drafts) + .clickFinalizeAll(2) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 2) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) @@ -42,8 +49,8 @@ class BulkFinalizationTest { @Test fun whenThereAreDraftsWithConstraintViolations_marksFormsAsHavingErrors() { - rule.startAtMainMenu() - .copyForm("two-question-required.xml") + rule.withProject("http://example.com") + .copyForm("two-question-required.xml", "example.com") .startBlankForm("Two Question Required") .fillOut(QuestionAndAnswer("What is your name?", "Dan")) .pressBack(SaveOrDiscardFormDialog(MainMenuPage())) @@ -56,8 +63,8 @@ class BulkFinalizationTest { ) .clickDrafts(2) - .clickOptionsIcon(string.finalize_all_drafts) - .clickOnString(string.finalize_all_drafts) + .clickFinalizeAll(2) + .clickFinalize() .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_partial_success, 1, 1) .assertText("Two Question Required") .pressBack(MainMenuPage()) @@ -68,27 +75,28 @@ class BulkFinalizationTest { @Test fun whenADraftPreviouslyHadConstraintViolations_marksFormsAsHavingErrors() { - rule.startAtMainMenu() - .copyForm("two-question-required.xml") + rule.withProject("http://example.com") + .copyForm("two-question-required.xml", "example.com") .startBlankForm("Two Question Required") .fillOut(QuestionAndAnswer("What is your name?", "Dan")) .pressBack(SaveOrDiscardFormDialog(MainMenuPage())) .clickSaveChanges() .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_drafts) - .clickOnString(string.finalize_all_drafts) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) .clickOptionsIcon(string.finalize_all_drafts) .clickOnString(string.finalize_all_drafts) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage(false)) .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) } @Test fun doesNotFinalizeInstancesWithSavePoints() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .swipeToEndScreen() .clickSaveAsDraft() @@ -98,9 +106,9 @@ class BulkFinalizationTest { .killAndReopenApp(MainMenuPage()) .clickDrafts(false) - .clickOptionsIcon(string.finalize_all_drafts) - .clickOnString(string.finalize_all_drafts) - .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) + .clickFinalizeAll(1) + .clickFinalize() + .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("One Question") .pressBack(MainMenuPage()) @@ -108,22 +116,98 @@ class BulkFinalizationTest { .assertNumberOfFinalizedForms(0) } + @Test + fun doesNotFinalizeInstancesFromEncryptedForms() { + rule.withProject("http://example.com") + .copyForm("encrypted.xml", "example.com") + .startBlankForm("encrypted") + .swipeToEndScreen() + .clickSaveAsDraft() + + .clickDrafts(1) + .clickFinalizeAll(1) + .clickFinalize() + .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) + .assertText("encrypted") + .pressBack(MainMenuPage()) + + .assertNumberOfEditableForms(1) + .assertNumberOfFinalizedForms(0) + } + @Test fun doesNotFinalizeAlreadyFinalizedInstances() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .startBlankForm("One Question") .fillOutAndFinalize(QuestionAndAnswer("what is your age", "98")) .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_drafts) - .clickOnString(string.finalize_all_drafts) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 1) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) .assertNumberOfFinalizedForms(2) } + + @Test + fun whenAutoSendIsEnabled_draftsAreSentAfterFinalizing() { + val mainMenuPage = rule.withProject(testDependencies.server.url) + .enableAutoSend() + + .copyForm("one-question.xml", testDependencies.server.hostName) + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) + + .clickDrafts(1) + .clickFinalizeAll(1) + .clickFinalize() + .pressBack(MainMenuPage()) + + testDependencies.scheduler.runDeferredTasks() + + mainMenuPage.clickViewSentForm(1) + .assertText("One Question") + + assertThat(testDependencies.server.submissions.size, equalTo(1)) + } + + @Test + fun canCancel() { + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) + + .clickDrafts(1) + .clickFinalizeAll(1) + .clickCancel() + .assertText("One Question") + .pressBack(MainMenuPage()) + + .assertNumberOfEditableForms(1) + } + + @Test + fun canBeDisabled() { + rule.withProject("http://example.com") + .openProjectSettingsDialog() + .clickSettings() + .clickAccessControl() + .clickFormEntrySettings() + .clickOnString(string.finalize_all_drafts) + .pressBack(AccessControlPage()) + .pressBack(ProjectSettingsPage()) + .pressBack(MainMenuPage()) + + .copyForm("one-question.xml", "example.com") + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "1892")) + .clickDrafts() + .assertNoOptionsMenu() + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt new file mode 100644 index 00000000000..4585fea8df6 --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt @@ -0,0 +1,30 @@ +package org.odk.collect.android.support.pages + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.odk.collect.strings.R +import org.odk.collect.strings.R.plurals +import org.odk.collect.strings.localization.getLocalizedQuantityString + +class BulkFinalizationConfirmationDialogPage(private val count: Int) : Page() { + override fun assertOnPage(): BulkFinalizationConfirmationDialogPage { + val title = ApplicationProvider.getApplicationContext() + .getLocalizedQuantityString(plurals.bulk_finalize_confirmation, count, count) + + onView(withText(title)).inRoot(isDialog()).check(matches(isDisplayed())) + return this + } + + fun clickFinalize(): EditSavedFormPage { + return this.clickOnButtonInDialog(R.string.finalize, EditSavedFormPage(false)) + } + + fun clickCancel(): EditSavedFormPage { + return this.clickOnButtonInDialog(R.string.cancel, EditSavedFormPage(false)) + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java index 5cd434f1cee..84f07c96db0 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java @@ -16,13 +16,6 @@ package org.odk.collect.android.support.pages; -import android.widget.RelativeLayout; - -import androidx.appcompat.widget.Toolbar; - -import org.odk.collect.android.R; -import org.odk.collect.android.adapters.InstanceListCursorAdapter; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -37,6 +30,13 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.not; +import android.widget.RelativeLayout; + +import androidx.appcompat.widget.Toolbar; + +import org.odk.collect.android.R; +import org.odk.collect.android.adapters.InstanceListCursorAdapter; + public class EditSavedFormPage extends Page { private final boolean firstOpen; @@ -106,4 +106,11 @@ public EditSavedFormPage searchInBar(String query) { onView(withId(androidx.appcompat.R.id.search_src_text)).perform(replaceText(query)); return this; } + + public BulkFinalizationConfirmationDialogPage clickFinalizeAll(int count) { + this.clickOptionsIcon(org.odk.collect.strings.R.string.finalize_all_drafts) + .clickOnString(org.odk.collect.strings.R.string.finalize_all_drafts); + + return new BulkFinalizationConfirmationDialogPage(count).assertOnPage(); + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 656fb540ce4..f45565aac17 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -2,6 +2,7 @@ package org.odk.collect.android.support.pages import android.app.Application import android.content.pm.ActivityInfo +import android.view.View import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider @@ -34,6 +35,8 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.not +import org.hamcrest.Matcher +import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf import org.hamcrest.core.StringContains.containsString import org.hamcrest.core.StringEndsWith.endsWith @@ -42,7 +45,6 @@ import org.odk.collect.android.BuildConfig import org.odk.collect.android.R import org.odk.collect.android.application.Collect import org.odk.collect.android.storage.StoragePathProvider -import org.odk.collect.android.support.ActivityHelpers import org.odk.collect.android.support.CollectHelpers import org.odk.collect.android.support.WaitFor.wait250ms import org.odk.collect.android.support.WaitFor.waitFor @@ -458,7 +460,7 @@ abstract class Page> { fun clickOptionsIcon(expectedOptionString: String): T { tryAgainOnFail({ - Espresso.openActionBarOverflowOrOptionsMenu(ActivityHelpers.getActivity()) + onView(OVERFLOW_BUTTON_MATCHER).perform(click()) assertText(expectedOptionString) }) @@ -488,6 +490,11 @@ abstract class Page> { return destination!!.assertOnPage() } + fun assertNoOptionsMenu(): T { + onView(OVERFLOW_BUTTON_MATCHER).check(doesNotExist()) + return this as T + } + companion object { private fun rotateToLandscape(): ViewAction { return RotateAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) @@ -496,5 +503,10 @@ abstract class Page> { private fun rotateToPortrait(): ViewAction { return RotateAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) } + + private val OVERFLOW_BUTTON_MATCHER: Matcher = Matchers.anyOf( + allOf(isDisplayed(), withContentDescription("More options")), + allOf(isDisplayed(), withClassName(Matchers.endsWith("OverflowMenuButton"))) + ) } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index 35dc9c517ff..e95587c7b97 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -52,12 +52,12 @@ import org.odk.collect.android.formmanagement.drafts.BulkFinalizationViewModel; import org.odk.collect.android.formmanagement.drafts.DraftsMenuProvider; import org.odk.collect.android.injection.DaggerUtils; +import org.odk.collect.android.instancemanagement.FinalizeAllSnackbarPresenter; import org.odk.collect.android.projects.ProjectsDataService; import org.odk.collect.android.utilities.ApplicationConstants; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.android.views.EmptyListView; -import org.odk.collect.androidshared.ui.SnackbarUtils; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.async.Scheduler; import org.odk.collect.forms.Form; @@ -65,15 +65,12 @@ import org.odk.collect.material.MaterialProgressDialogFragment; import org.odk.collect.settings.SettingsProvider; import org.odk.collect.settings.keys.MetaKeys; -import org.odk.collect.strings.R.plurals; import org.odk.collect.strings.R.string; import java.util.Arrays; import javax.inject.Inject; -import kotlin.Pair; - /** * Responsible for displaying all the valid instances in the instance directory. * @@ -165,49 +162,29 @@ public void onCreate(Bundle savedInstanceState) { BulkFinalizationViewModel bulkFinalizationViewModel = new BulkFinalizationViewModel( scheduler, - instancesDataService + instancesDataService, + settingsProvider ); - DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(bulkFinalizationViewModel); - addMenuProvider(draftsMenuProvider); - MaterialProgressDialogFragment.showOn(this, bulkFinalizationViewModel.isFinalizing(), getSupportFragmentManager(), () -> { MaterialProgressDialogFragment dialog = new MaterialProgressDialogFragment(); dialog.setMessage("Finalizing drafts..."); return dialog; }); - bulkFinalizationViewModel.getFinalizedForms().observe(this, finalizedForms -> { - if (!finalizedForms.isConsumed()) { - Pair pair = finalizedForms.getValue(); - if (pair.getSecond().equals(0)) { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getResources().getQuantityString( - plurals.bulk_finalize_success, - pair.getFirst(), - pair.getFirst() - ) - ); - } else if (pair.getFirst().equals(pair.getSecond())) { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getResources().getQuantityString( - plurals.bulk_finalize_failure, - pair.getFirst(), - pair.getFirst() - ) - ); - } else { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getString(string.bulk_finalize_partial_success, pair.getFirst() - pair.getSecond(), pair.getSecond()) - ); - } - - finalizedForms.consume(); - } - }); + if (bulkFinalizationViewModel.isEnabled()) { + DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts); + addMenuProvider(draftsMenuProvider, this); + bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> { + draftsMenuProvider.setDraftsCount(draftsCount); + invalidateMenu(); + }); + + bulkFinalizationViewModel.getFinalizedForms().observe( + this, + new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this) + ); + } } private void init() { diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 9c407a87053..72f79ea83cc 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -178,4 +178,10 @@ object AnalyticsEvents { * Tracks how often a form is finalized using a `ref` attribute on the `submission` element */ const val PARTIAL_FORM_FINALIZED = "PartialFormFinalized" + + /** + * Tracks how often drafts that can't be bulk finalized are attempted to be + */ + const val BULK_FINALIZE_ENCRYPTED_FORM = "BulkFinalizeEncryptedForm" + const val BULK_FINALIZE_SAVE_POINT = "BulkFinalizeSavePoint" } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt index 1d53b51da5f..4e1442c8400 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt @@ -18,7 +18,7 @@ class FormEndViewModel( } fun isFinalizeEnabled(): Boolean { - return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_FINALIZE) + return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY) } fun shouldFormBeSentAutomatically(): Boolean { diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index 65f16d8a504..d270d45d144 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -1,9 +1,13 @@ package org.odk.collect.android.formmanagement import androidx.lifecycle.LiveData +import org.odk.collect.analytics.Analytics +import org.odk.collect.android.analytics.AnalyticsEvents import org.odk.collect.android.application.Collect +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.entities.EntitiesRepositoryProvider import org.odk.collect.android.formentry.FormEntryUseCases +import org.odk.collect.android.projects.ProjectsDataService import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.storage.StorageSubdirectory import org.odk.collect.android.utilities.ExternalizableFormDefCache @@ -19,6 +23,8 @@ class InstancesDataService( private val instancesRepositoryProvider: InstancesRepositoryProvider, private val entitiesRepositoryProvider: EntitiesRepositoryProvider, private val storagePathProvider: StoragePathProvider, + private val instanceSubmitScheduler: InstanceSubmitScheduler, + private val projectsDataService: ProjectsDataService, private val onUpdate: () -> Unit ) { val editableCount: LiveData = appState.getLive(EDITABLE_COUNT_KEY, 0) @@ -49,11 +55,12 @@ class InstancesDataService( onUpdate() } - fun finalizeAllDrafts(): Pair { + fun finalizeAllDrafts(): FinalizeAllResult { val instancesRepository = instancesRepositoryProvider.get() val formsRepository = formsRepositoryProvider.get() val entitiesRepository = entitiesRepositoryProvider.get() val projectRootDir = File(storagePathProvider.getProjectRootDirPath()) + val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) val instances = instancesRepository.getAllByStatus( Instance.STATUS_INCOMPLETE, @@ -61,7 +68,7 @@ class InstancesDataService( Instance.STATUS_VALID ) - val totalFailed = instances.fold(0) { failCount, instance -> + val result = instances.fold(FinalizeAllResult(0, 0, false)) { result, instance -> val (formDef, form) = FormEntryUseCases.loadFormDef( instance, formsRepository, @@ -74,30 +81,36 @@ class InstancesDataService( CollectFormEntryControllerFactory().create(formDef, formMediaDir) val formController = FormEntryUseCases.loadDraft(form, instance, formEntryController) - val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) - val newFailCount = - if (FormEntryUseCases.getSavePoint(formController, File(cacheDir)) == null) { - val finalizedInstance = FormEntryUseCases.finalizeDraft( - formController, - instancesRepository, - entitiesRepository - ) - - if (finalizedInstance == null) { - failCount + 1 - } else { - failCount - } + val savePoint = FormEntryUseCases.getSavePoint(formController, File(cacheDir)) + val needsEncrypted = form.basE64RSAPublicKey != null + val newResult = if (savePoint != null) { + Analytics.log(AnalyticsEvents.BULK_FINALIZE_SAVE_POINT) + result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) + } else if (needsEncrypted) { + Analytics.log(AnalyticsEvents.BULK_FINALIZE_ENCRYPTED_FORM) + result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) + } else { + val finalizedInstance = FormEntryUseCases.finalizeDraft( + formController, + instancesRepository, + entitiesRepository + ) + + if (finalizedInstance == null) { + result.copy(failureCount = result.failureCount + 1) } else { - failCount + 1 + result } + } Collect.getInstance().externalDataManager?.close() - newFailCount + newResult } update() - return Pair(instances.size, totalFailed) + instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().uuid) + + return result.copy(successCount = instances.size - result.failureCount) } companion object { @@ -106,3 +119,5 @@ class InstancesDataService( private const val SENT_COUNT_KEY = "instancesSentCount" } } + +data class FinalizeAllResult(val successCount: Int, val failureCount: Int, val unsupportedInstances: Boolean) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt index 72f1b4ae644..7e96f787d12 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt @@ -2,22 +2,30 @@ package org.odk.collect.android.formmanagement.drafts import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import org.odk.collect.android.formmanagement.FinalizeAllResult import org.odk.collect.android.formmanagement.InstancesDataService import org.odk.collect.androidshared.data.Consumable import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.livedata.NonNullLiveData import org.odk.collect.async.Scheduler +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProtectedProjectKeys class BulkFinalizationViewModel( private val scheduler: Scheduler, - private val instancesDataService: InstancesDataService + private val instancesDataService: InstancesDataService, + private val settingsProvider: SettingsProvider ) { - private val _finalizedForms = MutableLiveData>>() - val finalizedForms: LiveData>> = _finalizedForms + private val _finalizedForms = MutableLiveData>() + val finalizedForms: LiveData> = _finalizedForms private val _isFinalizing = MutableNonNullLiveData(false) val isFinalizing: NonNullLiveData = _isFinalizing + val draftsCount = instancesDataService.editableCount + val isEnabled = + settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE) + fun finalizeAllDrafts() { _isFinalizing.value = true diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt index 92187a510b0..e09f4f928a9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt @@ -1,19 +1,51 @@ package org.odk.collect.android.formmanagement.drafts +import android.content.Context import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R +import org.odk.collect.strings.R.plurals +import org.odk.collect.strings.R.string + +class DraftsMenuProvider( + private val context: Context, + private val onFinalizeAll: Runnable +) : MenuProvider { + + var draftsCount: Int? = null -class DraftsMenuProvider(private val bulkFinalizationViewModel: BulkFinalizationViewModel) : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.drafts, menu) } + override fun onPrepareMenu(menu: Menu) { + if (draftsCount == null || draftsCount == 0) { + menu.findItem(R.id.bulk_finalize).isVisible = false + } + } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.bulk_finalize) { - bulkFinalizationViewModel.finalizeAllDrafts() + draftsCount?.also { + val dialogTitle = context.resources.getQuantityString( + plurals.bulk_finalize_confirmation, + it, + it + ) + + MaterialAlertDialogBuilder(context) + .setTitle(dialogTitle) + .setMessage(string.bulk_finalize_explanation) + .setPositiveButton(string.finalize) { _, _ -> + onFinalizeAll.run() + } + .setNegativeButton(string.cancel, null) + .show() + } + return true } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index 8b81d6345e2..2d4c02f88b0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -460,7 +460,7 @@ public UUIDGenerator providesUUIDGenerator() { } @Provides - public InstancesDataService providesInstancesDataService(Application application, InstancesRepositoryProvider instancesRepositoryProvider, ProjectsDataService projectsDataService, FormsRepositoryProvider formsRepositoryProvider, EntitiesRepositoryProvider entitiesRepositoryProvider, StoragePathProvider storagePathProvider) { + public InstancesDataService providesInstancesDataService(Application application, InstancesRepositoryProvider instancesRepositoryProvider, ProjectsDataService projectsDataService, FormsRepositoryProvider formsRepositoryProvider, EntitiesRepositoryProvider entitiesRepositoryProvider, StoragePathProvider storagePathProvider, InstanceSubmitScheduler instanceSubmitScheduler) { Function0 onUpdate = () -> { application.getContentResolver().notifyChange( InstancesContract.getUri(projectsDataService.getCurrentProject().getUuid()), @@ -470,7 +470,7 @@ public InstancesDataService providesInstancesDataService(Application application return null; }; - return new InstancesDataService(getState(application), formsRepositoryProvider, instancesRepositoryProvider, entitiesRepositoryProvider, storagePathProvider, onUpdate); + return new InstancesDataService(getState(application), formsRepositoryProvider, instancesRepositoryProvider, entitiesRepositoryProvider, storagePathProvider, instanceSubmitScheduler, projectsDataService, onUpdate); } @Provides diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt new file mode 100644 index 00000000000..371a88af47e --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt @@ -0,0 +1,47 @@ +package org.odk.collect.android.instancemanagement + +import android.content.Context +import android.view.View +import org.odk.collect.android.formmanagement.FinalizeAllResult +import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarDetails +import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver +import org.odk.collect.strings.R + +class FinalizeAllSnackbarPresenter(parentView: View, private val context: Context) : + SnackbarPresenterObserver(parentView) { + + override fun getSnackbarDetails(value: FinalizeAllResult): SnackbarDetails { + return if (value.unsupportedInstances) { + SnackbarDetails( + context.getString( + R.string.bulk_finalize_unsupported, + value.successCount + ) + ) + } else if (value.failureCount == 0) { + SnackbarDetails( + context.resources.getQuantityString( + R.plurals.bulk_finalize_success, + value.successCount, + value.successCount + ) + ) + } else if (value.successCount == 0) { + SnackbarDetails( + context.resources.getQuantityString( + R.plurals.bulk_finalize_failure, + value.failureCount, + value.failureCount + ) + ) + } else { + SnackbarDetails( + context.getString( + R.string.bulk_finalize_partial_success, + value.successCount, + value.failureCount + ) + ) + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt index a49dac92a41..66fa6deea35 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt @@ -43,15 +43,15 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = - settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) && findPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked + settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) && findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? -> - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = newValue as Boolean + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = newValue as Boolean true } - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked - findPreference(ProtectedProjectKeys.KEY_FINALIZE).onPreferenceChangeListener = + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? -> findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = newValue as Boolean true @@ -62,18 +62,18 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false) - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, true) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_JUMP_TO, false) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR, ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE) findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = false findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = false findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = false - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = false + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = false findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isChecked = false findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isChecked = false findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked = false - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked = true + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked = true } private fun onMovingBackwardsEnabled() { @@ -81,7 +81,7 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = true findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = true findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = true - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = true + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = true } private fun findPreference(key: String): CheckBoxPreference { diff --git a/collect_app/src/main/res/xml/form_entry_access_preferences.xml b/collect_app/src/main/res/xml/form_entry_access_preferences.xml index 7a69e689d5d..d1884e1e061 100644 --- a/collect_app/src/main/res/xml/form_entry_access_preferences.xml +++ b/collect_app/src/main/res/xml/form_entry_access_preferences.xml @@ -8,10 +8,11 @@ android:title="@string/moving_backwards_title" app:iconSpaceReserved="false" /> - + app:iconSpaceReserved="false"> - + app:iconSpaceReserved="false"> - \ No newline at end of file + + + + + diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt index ea8db74968f..aaedf4bef07 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt @@ -35,13 +35,13 @@ class FormEndViewModelTest { @Test fun `when 'Finalize' is enabled, isFinalizeEnabled should return true`() { - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, true) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(true)) } @Test fun `when 'Finalize' is disabled, isFinalizeEnabled should return false`() { - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, false) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false) assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(false)) } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt new file mode 100644 index 00000000000..8693bf8342d --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt @@ -0,0 +1,45 @@ +package org.odk.collect.android.formmanagement.drafts + +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.view.menu.MenuBuilder +import androidx.fragment.app.FragmentActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.odk.collect.android.R +import org.odk.collect.android.support.CollectHelpers + +@RunWith(AndroidJUnit4::class) +class DraftsMenuProviderTest { + + private val activity = CollectHelpers.createThemedActivity(FragmentActivity::class.java) + private val menuInflater = SupportMenuInflater(activity) + private val menu = MenuBuilder(activity) + private val draftsMenuProvider = DraftsMenuProvider(activity, mock()).also { + it.onCreateMenu(menu, menuInflater) + } + + @Test + fun whenDraftCountHasNotLoaded_doesNotShowFinalizeAll() { + draftsMenuProvider.draftsCount = null + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) + } + + @Test + fun whenDraftCountIsZero_doesNotShowFinalizeAll() { + draftsMenuProvider.draftsCount = 0 + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) + } + + @Test + fun whenDraftsCountIsNonZero_showsFinalizeAll() { + draftsMenuProvider.draftsCount = 1 + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(true)) + } +} diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt index 834657fbe90..634cdd1f99e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt @@ -32,7 +32,7 @@ class FormEntryAccessPreferencesFragmentTest { @Test fun `when the 'Save as draft' option is unchecked, the 'Finalize' option can't be changed`() { adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false) - adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE, true) + adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> @@ -42,7 +42,7 @@ class FormEntryAccessPreferencesFragmentTest { ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(false) ) } @@ -51,7 +51,7 @@ class FormEntryAccessPreferencesFragmentTest { @Test fun `when the 'Finalize' option is unchecked, the 'Save as draft' option can't be changed`() { adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true) - adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE, false) + adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false) val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> @@ -61,7 +61,7 @@ class FormEntryAccessPreferencesFragmentTest { ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(true) ) } @@ -72,18 +72,18 @@ class FormEntryAccessPreferencesFragmentTest { val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(true) ) fragment.getPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).performClick() assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(false) ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked, equalTo(true) ) } @@ -98,7 +98,7 @@ class FormEntryAccessPreferencesFragmentTest { equalTo(true) ) - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).performClick() + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).performClick() assertThat( fragment.getPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled, diff --git a/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java b/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java index 60f0579af3a..ccd0ec4bed5 100644 --- a/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java +++ b/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java @@ -155,17 +155,17 @@ public List getProtectedMigrations() { .withValues(false, false) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ) .withValues(false, true) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true ) .withValues(false, null) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true ), removeKey("mark_as_finalized"), removeKey("default_completed"), @@ -173,8 +173,14 @@ public List getProtectedMigrations() { .withValues(false) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true - ) + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true + ), + updateKeys("finalize").withValues(false) + .toPairs( + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false, + ProtectedProjectKeys.KEY_BULK_FINALIZE, false + ), + removeKey("finalize") ); } } diff --git a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt index f746c401d28..ae373d02f17 100644 --- a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt +++ b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt @@ -40,9 +40,10 @@ object ProtectedProjectKeys { const val KEY_JUMP_TO = "jump_to" const val KEY_SAVE_MID = "save_mid" const val KEY_SAVE_AS_DRAFT = "save_as_draft" - const val KEY_FINALIZE = "finalize" + const val KEY_FINALIZE_IN_FORM_ENTRY = "finalize_in_form_entry" const val ALLOW_OTHER_WAYS_OF_EDITING_FORM = "allow_other_ways_of_editing_form" + const val KEY_BULK_FINALIZE = "bulk_finalize" fun allKeys() = listOf( KEY_ADMIN_PW, @@ -81,7 +82,8 @@ object ProtectedProjectKeys { KEY_JUMP_TO, KEY_SAVE_MID, KEY_SAVE_AS_DRAFT, - KEY_FINALIZE, - ALLOW_OTHER_WAYS_OF_EDITING_FORM + KEY_FINALIZE_IN_FORM_ENTRY, + ALLOW_OTHER_WAYS_OF_EDITING_FORM, + KEY_BULK_FINALIZE ) } diff --git a/settings/src/main/resources/client-settings.schema.json b/settings/src/main/resources/client-settings.schema.json index d1fb73ab25e..60c74a923ab 100644 --- a/settings/src/main/resources/client-settings.schema.json +++ b/settings/src/main/resources/client-settings.schema.json @@ -321,6 +321,13 @@ "type": "boolean" }, "finalize": { + "type": "boolean", + "deprecated": true + }, + "finalize_in_form_entry": { + "type": "boolean" + }, + "bulk_finalize": { "type": "boolean" } } diff --git a/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java b/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java index ff949f415f4..45c07635971 100644 --- a/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java +++ b/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java @@ -260,7 +260,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasDisabled_th runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, ProtectedProjectKeys.KEY_FINALIZE, false); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -273,7 +273,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasEnabled_the runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE, true); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -285,7 +285,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasNotSet_then runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE, true); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -322,7 +322,7 @@ public void when_AllowOtherWaysOfEditingFormIsDisabled_thenSaveAsDraftShouldBeDi initSettings(protectedSettings, ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ); runMigrations(); @@ -330,7 +330,7 @@ public void when_AllowOtherWaysOfEditingFormIsDisabled_thenSaveAsDraftShouldBeDi assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(false)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(false)); - assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true)); } @Test @@ -338,7 +338,7 @@ public void when_AllowOtherWaysOfEditingFormIsEnabled_thenDoNotUpdateSaveAsDraft initSettings(protectedSettings, ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, true, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ); runMigrations(); @@ -346,7 +346,28 @@ public void when_AllowOtherWaysOfEditingFormIsEnabled_thenDoNotUpdateSaveAsDraft assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(true)); - assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE), equalTo(false)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false)); + } + + @Test + public void migratesFinalizeInFormEntryToNewKey() { + initSettings(protectedSettings, "finalize", false); + + runMigrations(); + + assertThat(protectedSettings.contains("finalize"), equalTo(false)); + assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false)); + } + + @Test + public void whenFinalizeInFormEntryWasDisabledWithOldKey_disablesBulkFinalize() { + initSettings(protectedSettings, "finalize", false); + + runMigrations(); + + assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(false)); } private void runMigrations() { diff --git a/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt b/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt index bb2f7e2b9db..40a48fb1320 100644 --- a/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt +++ b/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt @@ -7,8 +7,8 @@ import org.junit.Test class OriginalJsonSchemaSettingsValidatorTest { /* - * 'default_completed' and 'mark_as_finalized' were replaced by new settings in v2023.2 but - * we need the schema to still recognize the old fields so that we can migrate them correctly. + * Some settings end up replaced by new settings in but we need the schema to still + * recognize the old fields so that we can migrate them correctly. */ @Test fun `isValueSupported returns true for fields we no longer use`() { @@ -16,14 +16,17 @@ class OriginalJsonSchemaSettingsValidatorTest { javaClass.getResourceAsStream("/client-settings.schema.json")!! } - assertThat( - validator.isValueSupported("general", "default_completed", "true"), - equalTo(true) - ) - - assertThat( - validator.isValueSupported("admin", "mark_as_finalized", "true"), - equalTo(true) - ) + removedKeys.forEach { + assertThat( + validator.isValueSupported(it.first, it.second, "true"), + equalTo(true) + ) + } } + + private val removedKeys = listOf( + Pair("admin", "mark_as_finalized"), + Pair("general", "default_completed"), + Pair("admin", "finalize") + ) }