From 0da58360d616f902091e9f5eb82b9d5fb22821be Mon Sep 17 00:00:00 2001 From: Xing Wang Date: Wed, 8 Nov 2023 15:01:15 +0100 Subject: [PATCH] Detects the unsaved changes in the already confirmed steps. (#537) This PR detects the unsaved changes in the already confirmed steps. If there are any unsaved changes, it will block the submit step and show the blocker messages there. This is achieved by observing the selected_index of the _wizard_app_widget, and comparing the related values. --------- Co-authored-by: Jusong Yu Co-authored-by: Miki Bonacci --- src/aiidalab_qe/app/configuration/__init__.py | 7 ++++ src/aiidalab_qe/app/main.py | 34 ++++++++++++++++--- src/aiidalab_qe/app/structure/__init__.py | 5 +++ src/aiidalab_qe/app/submission/__init__.py | 18 ++++++---- tests/test_app.py | 31 +++++++++++++++++ tests/test_codes.py | 2 +- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/__init__.py b/src/aiidalab_qe/app/configuration/__init__.py index f0f09747b..e943d23c1 100644 --- a/src/aiidalab_qe/app/configuration/__init__.py +++ b/src/aiidalab_qe/app/configuration/__init__.py @@ -146,6 +146,13 @@ def confirm(self, _=None): self.confirm_button.disabled = False self.state = self.State.SUCCESS + def is_saved(self): + """Check if the current step is saved. + That all changes are confirmed. + """ + new_parameters = self.get_configuration_parameters() + return new_parameters == self.configuration_parameters + @tl.default("state") def _default_state(self): return self.State.INIT diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index 3a5c63e34..71b12a3b0 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -24,7 +24,8 @@ def __init__(self, qe_auto_setup=True): self.structure_step.observe(self._observe_structure_selection, "structure") self.configure_step = ConfigureQeAppWorkChainStep(auto_advance=True) self.submit_step = SubmitQeAppWorkChainStep( - auto_advance=True, qe_auto_setup=qe_auto_setup + auto_advance=True, + qe_auto_setup=qe_auto_setup, ) self.results_step = ViewQeAppWorkChainStatusAndResultsStep() @@ -49,7 +50,6 @@ def __init__(self, qe_auto_setup=True): (self.configure_step, "configuration_parameters"), (self.submit_step, "input_parameters"), ) - ipw.dlink( (self.submit_step, "process"), (self.results_step, "process"), @@ -65,6 +65,7 @@ def __init__(self, qe_auto_setup=True): ("Status & Results", self.results_step), ] ) + self._wizard_app_widget.observe(self._observe_selected_index, "selected_index") # Add process selection header self.work_chain_selector = QeAppWorkChainSelector( @@ -85,14 +86,39 @@ def __init__(self, qe_auto_setup=True): ] ) - # Reset all subsequent steps in case that a new structure is selected + @property + def steps(self): + return self._wizard_app_widget.steps + + # Reset the confirmed_structure in case that a new structure is selected def _observe_structure_selection(self, change): with self.structure_step.hold_sync(): if ( self.structure_step.confirmed_structure is not None and self.structure_step.confirmed_structure != change["new"] ): - self._wizard_app_widget.reset() + self.structure_step.confirmed_structure = None + + def _observe_selected_index(self, change): + """Check unsaved change in the step when leaving the step.""" + # no accordion tab is selected + if not change["new"]: + return + new_idx = change["new"] + # only when entering the submit step, check and udpate the blocker messages + # steps[new_idx][0] is the title of the step + if self.steps[new_idx][1] is not self.submit_step: + return + blockers = [] + # Loop over all steps before the submit step + for title, step in self.steps[:new_idx]: + # check if the step is saved + if not step.is_saved(): + step.state = WizardAppWidgetStep.State.CONFIGURED + blockers.append( + f"Unsaved changes in the {title} step. Please save the changes before submitting." + ) + self.submit_step.external_submission_blockers = blockers def _observe_process_selection(self, change): from aiida.orm.utils.serialize import deserialize_unsafe diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index 306fa5234..a1b915454 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -162,6 +162,11 @@ def confirm(self, _=None): self.confirmed_structure = self.structure self.message_area.value = "" + def is_saved(self): + """Check if the current structure is saved. + That all changes are confirmed.""" + return self.confirmed_structure == self.structure + def can_reset(self): return self.confirmed_structure is not None diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index e3a8f9602..dbe247c30 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -60,7 +60,8 @@ class SubmitQeAppWorkChainStep(ipw.VBox, WizardAppWidgetStep): process = tl.Instance(orm.WorkChainNode, allow_none=True) previous_step_state = tl.UseEnum(WizardAppWidgetStep.State) input_parameters = tl.Dict() - _submission_blockers = tl.List(tl.Unicode()) + internal_submission_blockers = tl.List(tl.Unicode()) + external_submission_blockers = tl.List(tl.Unicode()) def __init__(self, qe_auto_setup=True, **kwargs): self.message_area = ipw.Output() @@ -138,10 +139,12 @@ def __init__(self, qe_auto_setup=True, **kwargs): ] ) - @tl.observe("_submission_blockers") - def _observe_submission_blockers(self, change): - if change["new"]: - fmt_list = "\n".join((f"
  • {item}
  • " for item in sorted(change["new"]))) + @tl.observe("internal_submission_blockers", "external_submission_blockers") + def _observe_submission_blockers(self, _change): + """Observe the submission blockers and update the message area.""" + blockers = self.internal_submission_blockers + self.external_submission_blockers + if any(blockers): + fmt_list = "\n".join((f"
  • {item}
  • " for item in sorted(blockers))) self._submission_blocker_messages.value = f"""
    The submission is blocked, due to the following reason(s): @@ -150,6 +153,7 @@ def _observe_submission_blockers(self, change): self._submission_blocker_messages.value = "" def _identify_submission_blockers(self): + """Validate the resource inputs and identify blockers for the submission.""" # Do not submit while any of the background setup processes are running. if self.qe_setup_status.busy or self.sssp_installation_status.busy: yield "Background setup processes must finish." @@ -184,11 +188,11 @@ def _update_state(self, _=None): blockers = list(self._identify_submission_blockers()) if any(blockers): - self._submission_blockers = blockers + self.internal_submission_blockers = blockers self.state = self.State.READY return - self._submission_blockers = [] + self.internal_submission_blockers = [] self.state = self.state.CONFIGURED def _toggle_install_widgets(self, change): diff --git a/tests/test_app.py b/tests/test_app.py index 38cd75e9a..256b1cb64 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -33,3 +33,34 @@ def test_reload_and_reset(submit_app_generator, generate_qeapp_workchain): == 0 ) assert app.submit_step.resources_config.num_cpus.value == 1 + + +def test_select_new_structure(app_to_submit, generate_structure_data): + """Test if the new structure is selected, the confirmed structure is reset""" + app = app_to_submit + assert app.structure_step.confirmed_structure is not None + # select a new structure will reset the confirmed structure + app.structure_step.structure = generate_structure_data() + assert app.structure_step.confirmed_structure is None + + +def test_unsaved_changes(app_to_submit): + """Test if the unsaved changes are handled correctly""" + from aiidalab_widgets_base import WizardAppWidgetStep + + app = app_to_submit + # go to the configue step, and make some changes + app._wizard_app_widget.selected_index = 1 + app.configure_step.workchain_settings.relax_type.value = "positions" + # go to the submit step + app._wizard_app_widget.selected_index = 2 + # the state of the configue step should be updated. + assert app.configure_step.state == WizardAppWidgetStep.State.CONFIGURED + # check if a new blocker is added + assert len(app.submit_step.external_submission_blockers) == 1 + # confirm the changes + app._wizard_app_widget.selected_index = 1 + app.configure_step.confirm() + app._wizard_app_widget.selected_index = 2 + # the blocker should be removed + assert len(app.submit_step.external_submission_blockers) == 0 diff --git a/tests/test_codes.py b/tests/test_codes.py index d535d0cca..edaa9e576 100644 --- a/tests/test_codes.py +++ b/tests/test_codes.py @@ -21,7 +21,7 @@ def test_set_selected_codes(submit_app_generator): def test_update_codes_display(): - """Test update_codes_visibility method. + """Test update_codes_display method. If the workchain property is not selected, the related code should be hidden. """ from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep