Skip to content

Commit

Permalink
Add runpath validation in gui load_results_manually
Browse files Browse the repository at this point in the history
This commit adds runpath validation so the load button in
`load_results_manually` will be disabled until the runpath exists, and
the user has the read/execute permissions.
  • Loading branch information
jonathan-eq committed Oct 17, 2024
1 parent 45b9a36 commit 74e401e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 22 deletions.
2 changes: 2 additions & 0 deletions src/ert/gui/ertwidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def wrapper(*arg: Any) -> Any:
from .ensembleselector import EnsembleSelector
from .checklist import CheckList
from .stringbox import StringBox
from .multilinestringbox import MultiLineStringBox
from .listeditbox import ListEditBox
from .customdialog import CustomDialog
from .pathchooser import PathChooser
Expand All @@ -51,6 +52,7 @@ def wrapper(*arg: Any) -> Any:
"EnsembleSelector",
"ErtMessageBox",
"ListEditBox",
"MultiLineStringBox",
"PathChooser",
"PathModel",
"SearchBox",
Expand Down
112 changes: 112 additions & 0 deletions src/ert/gui/ertwidgets/multilinestringbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Optional

from qtpy.QtGui import QPalette
from qtpy.QtWidgets import QTextEdit

from .validationsupport import ValidationSupport

if TYPE_CHECKING:
from ert.validation import ArgumentDefinition

from .models import TextModel


class MultiLineStringBox(QTextEdit):
"""MultiLineStringBox shows a multiline string. The data structure expected and sent to the
getter and setter is a multiline string."""

def __init__(
self,
model: TextModel,
default_string: str = "",
placeholder_text: str = "",
minimum_width: int = 250,
readonly: bool = False,
):
QTextEdit.__init__(self)
self.setMinimumWidth(minimum_width)
self._validation = ValidationSupport(self)
self._validator: Optional[ArgumentDefinition] = None
self._model = model
self._enable_validation = True

if placeholder_text:
self.setPlaceholderText(placeholder_text)
self.textChanged.connect(self.stringBoxChanged)

self.textChanged.connect(self.validateString)

self._valid_color = self.palette().color(self.backgroundRole())
self.setText(default_string)

self._model.valueChanged.connect(self.modelChanged)
self.modelChanged()
self.setReadOnly(readonly)

def validateString(self) -> None:
if not self._enable_validation or self._validator is None:
return

string_to_validate = self.toPlainText()
if not string_to_validate and self.placeholderText():
string_to_validate = self.placeholderText()

validation_success = self._validator.validate(string_to_validate)

palette = self.palette()
if not validation_success:
palette.setColor(QPalette.ColorRole.Base, ValidationSupport.ERROR_COLOR)
self.setPalette(palette)
self._validation.setValidationMessage(
str(validation_success), ValidationSupport.EXCLAMATION
)
else:
palette.setColor(QPalette.ColorRole.Base, self._valid_color)
self.setPalette(palette)
self._validation.setValidationMessage("")

def emitChange(self, q_string: Any) -> None:
self.textChanged.emit(str(q_string))

def stringBoxChanged(self) -> None:
"""Called whenever the contents of the textedit changes."""
text: Optional[str] = self.get_text
if not text:
text = None

self._model.setValue(text)

def modelChanged(self) -> None:
"""Retrieves data from the model and inserts it into the textedit"""
text = self._model.getValue()
if text is None:
text = ""
# If model and view has same text, return
if text == self.toPlainText():
return
self.setText(str(text))

@property
def model(self) -> TextModel:
return self._model

def setValidator(self, validator: ArgumentDefinition) -> None:
self._validator = validator

def getValidationSupport(self) -> ValidationSupport:
return self._validation

def isValid(self) -> bool:
return self._validation.isValid()

@property
def get_text(self) -> str:
return self.toPlainText() if self.toPlainText() else self.placeholderText()

def enable_validation(self, enabled: bool) -> None:
self._enable_validation = enabled

def refresh(self) -> None:
self.validateString()
32 changes: 22 additions & 10 deletions src/ert/gui/tools/load_results/load_results_panel.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from __future__ import annotations

from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QFormLayout, QMessageBox, QTextEdit, QWidget
from qtpy.QtWidgets import QFormLayout, QMessageBox, QWidget

from ert.gui.ertnotifier import ErtNotifier
from ert.gui.ertwidgets import (
ActiveRealizationsModel,
EnsembleSelector,
ErtMessageBox,
MultiLineStringBox,
QApplication,
StringBox,
ValueModel,
)
from ert.libres_facade import LibresFacade
from ert.run_models.base_run_model import captured_logs
from ert.validation import IntegerArgument, RangeStringArgument
from ert.validation import IntegerArgument, RangeStringArgument, RunPathArgument


class LoadResultsPanel(QWidget):
Expand All @@ -34,13 +35,18 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier):

layout = QFormLayout()

run_path_text = QTextEdit()
run_path_text.setText(self.readCurrentRunPath())
run_path_text.setDisabled(True)
run_path_text.setFixedHeight(80)

layout.addRow("Load data from current run path: ", run_path_text)
self.run_path_text_model = ValueModel()
self._run_path_field = MultiLineStringBox(
self.run_path_text_model, # type: ignore
default_string="",
readonly=True,
)
self._run_path_field.setValidator(RunPathArgument())
self._run_path_field.setObjectName("run_path_field_lrm")
self._run_path_field.setFixedHeight(80)
self._run_path_field.setText(self.readCurrentRunPath())

layout.addRow("Load data from current run path: ", self._run_path_field)
ensemble_selector = EnsembleSelector(self._notifier)
layout.addRow("Load into ensemble:", ensemble_selector)
self._ensemble_selector = ensemble_selector
Expand All @@ -66,7 +72,9 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier):
self._iterations_field.setValidator(IntegerArgument(from_value=0))
self._iterations_field.setObjectName("iterations_field_lrm")
layout.addRow("Iteration to load:", self._iterations_field)

self._run_path_field.getValidationSupport().validationChanged.connect(
self.panelConfigurationChanged
)
self._active_realizations_field.getValidationSupport().validationChanged.connect(
self.panelConfigurationChanged
)
Expand All @@ -76,6 +84,9 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier):

self.setLayout(layout)

def refresh(self) -> None:
self._run_path_field.refresh()

def readCurrentRunPath(self) -> str:
current_ensemble = self._notifier.current_ensemble_name
run_path = self._facade.run_path
Expand All @@ -85,7 +96,8 @@ def readCurrentRunPath(self) -> str:

def isConfigurationValid(self) -> bool:
return (
self._active_realizations_field.isValid()
self._run_path_field.isValid()
and self._active_realizations_field.isValid()
and self._iterations_field.isValid()
)

Expand Down
29 changes: 17 additions & 12 deletions src/ert/gui/tools/load_results/load_results_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,24 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier) -> None:
def trigger(self) -> None:
if self._import_widget is None:
self._import_widget = LoadResultsPanel(self.facade, self._notifier)
self._dialog = ClosableDialog(
"Load results manually",
self._import_widget,
self.parent(), # type: ignore
)
self._dialog.setObjectName("load_results_manually_tool")
loadButton = self._dialog.addButton("Load", self.load)
self._import_widget.panelConfigurationChanged.connect(
self.validationStatusChanged
)
self._dialog = ClosableDialog(
"Load results manually",
self._import_widget,
self.parent(), # type: ignore
)
self._loadButton = self._dialog.addButton("Load", self.load)
self._dialog.setObjectName("load_results_manually_tool")

else:
self._import_widget.refresh()

if not self._import_widget._ensemble_selector.isEnabled():
loadButton.setEnabled(False)
loadButton.setToolTip("Must load into a ensemble")
self._import_widget.panelConfigurationChanged.connect(
self.validationStatusChanged
)
self._loadButton.setEnabled(False)
self._loadButton.setToolTip("Must load into a ensemble")
assert self._dialog is not None
self._dialog.exec_()

def load(self, _: Any) -> None:
Expand Down
2 changes: 2 additions & 0 deletions src/ert/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .proper_name_format_argument import ProperNameFormatArgument
from .range_string_argument import RangeStringArgument
from .rangestring import mask_to_rangestring, rangestring_to_list, rangestring_to_mask
from .runpath_argument import RunPathArgument
from .validation_status import ValidationStatus

__all__ = [
Expand All @@ -16,6 +17,7 @@
"ProperNameArgument",
"ProperNameFormatArgument",
"RangeStringArgument",
"RunPathArgument",
"ValidationStatus",
"mask_to_rangestring",
"rangestring_to_list",
Expand Down
27 changes: 27 additions & 0 deletions src/ert/validation/runpath_argument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

from .argument_definition import ArgumentDefinition
from .validation_status import ValidationStatus


class RunPathArgument(ArgumentDefinition):
INVALID_PATH = "The specified runpath does not exist."
MISSING_PERMISSION = "You are missing permissions for the specified runpath."

def __init__(self, **kwargs: bool) -> None:
super().__init__(**kwargs)

def validate(self, token: str) -> ValidationStatus:
parsed_runpath_without_suffix = "/".join(token.split("/")[:-2])
validation_status = super().validate(token)

if not os.path.isdir(parsed_runpath_without_suffix):
validation_status.setFailed()
validation_status.addToMessage(RunPathArgument.INVALID_PATH)
elif not os.access(parsed_runpath_without_suffix, os.R_OK | os.X_OK):
validation_status.setFailed()
validation_status.addToMessage(RunPathArgument.MISSING_PERMISSION)
else:
validation_status.setValue(token)

return validation_status

0 comments on commit 74e401e

Please sign in to comment.