Skip to content

Commit

Permalink
Replace DependentStep mixin with proper QeWizardStep MVC classes (#…
Browse files Browse the repository at this point in the history
…1032)

This PR resolves issues with #1004 (incorrect handling of step-to-step switching/rendering) by treating the app steps more uniformly. It also introduces a simple locking mechanism on submission that disconnects all observations of step confirmation, thus rendering the app in a "locked" state by permanently disabling confirm/submit buttons.
  • Loading branch information
edan-bainglass authored Dec 24, 2024
1 parent 771f408 commit a363bbc
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 186 deletions.
54 changes: 5 additions & 49 deletions src/aiidalab_qe/app/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.utils import get_entry_items
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.mixins import DependentStep
from aiidalab_qe.common.panel import (
ConfigurationSettingsModel,
ConfigurationSettingsPanel,
)
from aiidalab_widgets_base import WizardAppWidgetStep
from aiidalab_qe.common.widgets import QeDependentWizardStep

from .advanced import (
AdvancedConfigurationSettingsModel,
Expand All @@ -28,24 +27,11 @@
DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore


class ConfigureQeAppWorkChainStep(
ipw.VBox,
WizardAppWidgetStep,
DependentStep,
):
class ConfigureQeAppWorkChainStep(QeDependentWizardStep[ConfigurationStepModel]):
missing_information_warning = "Missing input structure. Please set it first."

previous_step_state = tl.UseEnum(WizardAppWidgetStep.State)

def __init__(self, model: ConfigurationStepModel, **kwargs):
from aiidalab_qe.common.widgets import LoadingWidget

super().__init__(
children=[LoadingWidget("Loading workflow configuration step")],
**kwargs,
)

self._model = model
super().__init__(model=model, **kwargs)
self._model.observe(
self._on_confirmation_change,
"confirmed",
Expand All @@ -55,21 +41,6 @@ def __init__(self, model: ConfigurationStepModel, **kwargs):
"input_structure",
)

self.rendered = False

self.structure_set_message = ipw.HTML()
ipw.dlink(
(self._model, "input_structure"),
(self.structure_set_message, "value"),
lambda structure: ""
if structure
else """
<div class="alert alert-danger">
<b>Please set the input structure first.</b>
</div>
""",
)

workchain_model = BasicConfigurationSettingsModel()
self.workchain_settings = BasicConfigurationSettingsPanel(model=workchain_model)
self._model.add_model("workchain", workchain_model)
Expand All @@ -89,10 +60,7 @@ def __init__(self, model: ConfigurationStepModel, **kwargs):

self._fetch_plugin_calculation_settings()

def render(self):
if self.rendered:
return

def _render(self):
# RelaxType: degrees of freedom in geometry optimization
self.relax_type_help = ipw.HTML()
ipw.dlink(
Expand Down Expand Up @@ -156,7 +124,6 @@ def render(self):

self.children = [
InAppGuide(identifier="configuration-step"),
self.structure_set_message,
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 0px">
<h4>Structure relaxation</h4>
Expand All @@ -168,20 +135,9 @@ def render(self):
self.confirm_button,
]

self.rendered = True

def _post_render(self):
self._update_tabs()

if self._model.confirmed: # loaded from a process
return

# NOTE technically not necessary, as an update is triggered
# by a structure change. However, this ensures that if a user
# decides to visit this step prior to setting the structure,
# the step will be updated on render to show reasonable defaults.
# TODO remove if we decide to "disable" steps past unconfirmed steps!
self._model.update()

def is_saved(self):
return self._model.confirmed

Expand Down
6 changes: 4 additions & 2 deletions src/aiidalab_qe/app/configuration/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@
HasInputStructure,
HasModels,
)
from aiidalab_qe.common.mvc import Model
from aiidalab_qe.common.panel import ConfigurationSettingsModel
from aiidalab_qe.common.widgets import QeWizardStepModel

DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore

NO_RELAXATION_OPTION = ("Structure as is", "none")


class ConfigurationStepModel(
Model,
QeWizardStepModel,
HasModels[ConfigurationSettingsModel],
HasInputStructure,
Confirmable,
):
identifier = "configuration"

relax_type_help = tl.Unicode()
relax_type_options = tl.List([NO_RELAXATION_OPTION])
relax_type = tl.Unicode(NO_RELAXATION_OPTION[-1], allow_none=True)
Expand Down
44 changes: 25 additions & 19 deletions src/aiidalab_qe/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep
from aiidalab_qe.app.submission.model import SubmissionStepModel
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.mixins import DependentStep
from aiidalab_qe.common.widgets import LoadingWidget
from aiidalab_qe.common.widgets import LoadingWidget, QeWizardStep
from aiidalab_widgets_base import WizardAppWidget


Expand Down Expand Up @@ -75,10 +74,9 @@ def __init__(self, qe_auto_setup=True):
(self.submit_step, "state"),
(self.results_step, "previous_step_state"),
)
ipw.dlink(
(self.submit_model, "process_node"),
(self.results_model, "process_uuid"),
lambda node: node.uuid if node is not None else None,
self.submit_model.observe(
self._on_submission,
"confirmed",
)

# Add the application steps to the application
Expand Down Expand Up @@ -139,20 +137,13 @@ def _on_configuration_confirmation_change(self, _):
self._update_submission_step()
self._update_blockers()

def _on_submission(self, _):
self._update_results_step()
self._lock_app()

def _render_step(self, step_index):
step = self.steps[step_index][1]
if step is self.configure_step and not self.structure_model.confirmed:
step.show_missing_information_warning()
elif step is self.submit_step and not self.configure_model.confirmed:
step.show_missing_information_warning()
elif step is self.results_step and not self.submit_model.confirmed:
step.show_missing_information_warning()
elif isinstance(step, DependentStep):
step.hide_missing_information_warning()
step.render()
step.previous_children = step.children
else:
step.render()
step: QeWizardStep = self.steps[step_index][1]
step.render()

def _update_configuration_step(self):
if self.structure_model.confirmed:
Expand All @@ -168,6 +159,21 @@ def _update_submission_step(self):
self.submit_model.input_structure = None
self.submit_model.input_parameters = {}

def _update_results_step(self):
ipw.dlink(
(self.submit_model, "process_node"),
(self.results_model, "process_uuid"),
lambda node: node.uuid if node is not None else None,
)

def _lock_app(self):
for model in (
self.structure_model,
self.configure_model,
self.submit_model,
):
model.unobserve_all("confirmed")

def _update_blockers(self):
self.submit_model.external_submission_blockers = [
f"Unsaved changes in the <b>{title}</b> step. Please confirm the changes before submitting."
Expand Down
37 changes: 6 additions & 31 deletions src/aiidalab_qe/app/result/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

from aiida.engine import ProcessState
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.mixins import DependentStep
from aiidalab_qe.common.widgets import LoadingWidget
from aiidalab_qe.common.widgets import LoadingWidget, QeDependentWizardStep
from aiidalab_widgets_base import ProcessMonitor, WizardAppWidgetStep

from .components import ResultsComponent
Expand All @@ -18,24 +17,13 @@
PROCESS_RUNNING = "<h4>Workflow is running!</h4>"


class ViewQeAppWorkChainStatusAndResultsStep(
ipw.VBox,
WizardAppWidgetStep,
DependentStep,
):
previous_step_state = tl.UseEnum(WizardAppWidgetStep.State)

class ViewQeAppWorkChainStatusAndResultsStep(QeDependentWizardStep[ResultsStepModel]):
missing_information_warning = (
"No available results. Did you submit or load a calculation?"
)

def __init__(self, model: ResultsStepModel, **kwargs):
super().__init__(
children=[LoadingWidget("Loading results step")],
**kwargs,
)

self._model = model
super().__init__(model=model, **kwargs)
self.observe(
self._on_previous_step_state_change,
"previous_step_state",
Expand All @@ -45,12 +33,7 @@ def __init__(self, model: ResultsStepModel, **kwargs):
"process_uuid",
)

self.rendered = False

def render(self):
if self.rendered:
return

def _render(self):
self.kill_button = ipw.Button(
description="Kill workchain",
tooltip="Kill the below workchain.",
Expand Down Expand Up @@ -130,17 +113,8 @@ def render(self):

if self._model.has_process:
self._update_children()
elif self.previous_step_state is not WizardAppWidgetStep.State.SUCCESS:
self.children = [
ipw.HTML("""
<div class="alert alert-danger" style="text-align: center;">
No process detected. Please submit a calculation.
</div>
"""),
]

self.rendered = True

def _post_render(self):
self._update_kill_button_layout()
self._update_clean_scratch_button_layout()

Expand Down Expand Up @@ -213,6 +187,7 @@ def _update_children(self):
self.toggle_controls,
self.container,
]
self.previous_children = list(self.children)

def _toggle_view(self, panel: ResultsComponent):
self.container.children = [panel]
Expand Down
6 changes: 4 additions & 2 deletions src/aiidalab_qe/app/result/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
from aiida import orm
from aiida.engine.processes import control
from aiidalab_qe.common.mixins import HasModels, HasProcess
from aiidalab_qe.common.mvc import Model
from aiidalab_qe.common.widgets import QeWizardStepModel

from .components import ResultsComponentModel


class ResultsStepModel(
Model,
QeWizardStepModel,
HasModels[ResultsComponentModel],
HasProcess,
):
identifier = "results"

process_info = tl.Unicode("")
process_remote_folder_is_clean = tl.Bool(False)

Expand Down
24 changes: 4 additions & 20 deletions src/aiidalab_qe/app/structure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@
LazyLoadedStructureBrowser,
)
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import CategorizedStructureExamplesWidget
from aiidalab_qe.common.widgets import CategorizedStructureExamplesWidget, QeWizardStep
from aiidalab_widgets_base import (
BasicCellEditor,
BasicStructureEditor,
StructureManagerWidget,
StructureUploadWidget,
WizardAppWidgetStep,
)

# The Examples list of (name, file) tuple curretly passed to
Expand All @@ -38,22 +37,15 @@
]


class StructureSelectionStep(ipw.VBox, WizardAppWidgetStep):
class StructureSelectionStep(QeWizardStep[StructureStepModel]):
"""Integrated widget for the selection and edition of structure.
The widget includes a structure manager that allows to select a structure
from different sources. It also includes the structure editor. Both the
structure importers and the structure editors can be extended by plugins.
"""

def __init__(self, model: StructureStepModel, **kwargs):
from aiidalab_qe.common.widgets import LoadingWidget

super().__init__(
children=[LoadingWidget("Loading structure selection step")],
**kwargs,
)

self._model = model
super().__init__(model=model, **kwargs)
self._model.observe(
self._on_confirmation_change,
"confirmed",
Expand All @@ -63,13 +55,7 @@ def __init__(self, model: StructureStepModel, **kwargs):
"input_structure",
)

self.rendered = False

def render(self):
"""docstring"""
if self.rendered:
return

def _render(self):
examples_by_category = {"Simple crystals": Examples}
plugin_structure_examples = {
item["title"]: item["structures"]
Expand Down Expand Up @@ -173,8 +159,6 @@ def render(self):
self.confirm_button,
]

self.rendered = True

def is_saved(self):
return self._model.confirmed

Expand Down
6 changes: 4 additions & 2 deletions src/aiidalab_qe/app/structure/model.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import traitlets as tl

from aiidalab_qe.common.mixins import Confirmable, HasInputStructure
from aiidalab_qe.common.mvc import Model
from aiidalab_qe.common.widgets import QeWizardStepModel


class StructureStepModel(
Model,
QeWizardStepModel,
HasInputStructure,
Confirmable,
):
identifier = "structure"

structure_name = tl.Unicode("")
manager_output = tl.Unicode("")
message_area = tl.Unicode("")
Expand Down
Loading

0 comments on commit a363bbc

Please sign in to comment.