From d915d2f507fa9063d3dd95ddba5184fd4a3ca9b7 Mon Sep 17 00:00:00 2001 From: David Perl Date: Fri, 16 May 2025 14:23:49 +0200 Subject: [PATCH] fix: (#612) fix additional MD form makes sure the form is validated on any changes of the additional metadata table model so that they are propagated to the scan control widget even when nothing is entered in the standard form --- .../widgets/editors/dict_backed_table.py | 7 +++ .../editors/scan_metadata/scan_metadata.py | 1 + tests/unit_tests/test_scan_control.py | 57 +++++++++++++++---- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/bec_widgets/widgets/editors/dict_backed_table.py b/bec_widgets/widgets/editors/dict_backed_table.py index 78a76b66..99a0439f 100644 --- a/bec_widgets/widgets/editors/dict_backed_table.py +++ b/bec_widgets/widgets/editors/dict_backed_table.py @@ -53,6 +53,7 @@ class DictBackedTableModel(QAbstractTableModel): if value in self._disallowed_keys or value in self._other_keys(index.row()): return False self._data[index.row()][index.column()] = str(value) + self.dataChanged.emit(index, index) return True return False @@ -109,6 +110,7 @@ class DictBackedTableModel(QAbstractTableModel): class DictBackedTable(QWidget): delete_rows = Signal(list) + data_updated = Signal() def __init__(self, initial_data: list[list[str]]): """Widget which uses a DictBackedTableModel to display an editable table @@ -141,6 +143,11 @@ class DictBackedTable(QWidget): self._add_button.clicked.connect(self._table_model.add_row) self._remove_button.clicked.connect(self.delete_selected_rows) self.delete_rows.connect(self._table_model.delete_rows) + self._table_model.dataChanged.connect(self._emit_data_updated) + + def _emit_data_updated(self, *args, **kwargs): + """Just to swallow the args""" + self.data_updated.emit() def delete_selected_rows(self): """Delete rows which are part of the selection model""" diff --git a/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py b/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py index 580878ba..918576d1 100644 --- a/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +++ b/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py @@ -43,6 +43,7 @@ class ScanMetadata(PydanticModelForm): self._additional_metadata = DictBackedTable(initial_extras or []) self._scan_name = scan_name or "" self._md_schema = get_metadata_schema_for_scan(self._scan_name) + self._additional_metadata.data_updated.connect(self.validate_form) super().__init__(parent=parent, metadata_model=self._md_schema, client=client, **kwargs) diff --git a/tests/unit_tests/test_scan_control.py b/tests/unit_tests/test_scan_control.py index 5248b995..d47f661b 100644 --- a/tests/unit_tests/test_scan_control.py +++ b/tests/unit_tests/test_scan_control.py @@ -1,9 +1,11 @@ # pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring -from unittest.mock import MagicMock +from types import SimpleNamespace +from unittest.mock import MagicMock, patch import pytest from bec_lib.endpoints import MessageEndpoints from bec_lib.messages import AvailableResourceMessage, ScanQueueHistoryMessage, ScanQueueMessage +from qtpy.QtCore import QModelIndex, QPoint, Qt from bec_widgets.utils.forms_from_types.items import StrMetadataField from bec_widgets.utils.widget_io import WidgetIO @@ -540,6 +542,29 @@ def test_get_scan_parameters_from_redis(scan_control, mocked_client): assert kwargs == {"steps": 10, "relative": False, "exp_time": 2.0, "burst_at_each_point": 1} +TEST_MD = {"sample_name": "Test Sample", "test key 1": "test value 1", "test key 2": "test value 2"} +TEST_TABLE_ENTRY = [["test key 1", "test value 1"], ["test key 2", "test value 2"]] + + +def test_scan_metadata_is_updated_even_without_default_form_changes( + scan_control: ScanControl, qtbot +): + assert scan_control._metadata_form._scan_name == "line_scan" + scan_control.comboBox_scan_selection.setCurrentText("grid_scan") + assert scan_control._metadata_form._scan_name == "grid_scan" + scan_control._metadata_form._additional_metadata._add_button.click() + qtbot.wait(100) + table_model = scan_control._metadata_form._additional_metadata._table_model + model_key = table_model.index(0, 0, QModelIndex()) + table_model.setData(model_key, "test key 1", Qt.EditRole) + model_value = model_key.siblingAtColumn(1) + table_model.setData(model_value, "test value 1", Qt.EditRole) + assert scan_control._metadata_form._additional_metadata.dump_dict() == { + "test key 1": "test value 1" + } + assert scan_control._scan_metadata == {"sample_name": "", "test key 1": "test value 1"} + + def test_scan_metadata_is_connected(scan_control): assert scan_control._metadata_form._scan_name == "line_scan" scan_control.comboBox_scan_selection.setCurrentText("grid_scan") @@ -548,16 +573,28 @@ def test_scan_metadata_is_connected(scan_control): assert isinstance(sample_name, StrMetadataField) sample_name._main_widget.setText("Test Sample") - scan_control._metadata_form._additional_metadata._table_model._data = [ - ["test key 1", "test value 1"], - ["test key 2", "test value 2"], - ] + scan_control._metadata_form._additional_metadata._table_model._data = TEST_TABLE_ENTRY scan_control._metadata_form.validate_form() - assert scan_control._scan_metadata == { - "sample_name": "Test Sample", - "test key 1": "test value 1", - "test key 2": "test value 2", - } + assert scan_control._scan_metadata == TEST_MD + + +def test_scan_metadata_is_passed_to_scan_function(scan_control: ScanControl): + scan_control.comboBox_scan_selection.setCurrentText("grid_scan") + + sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(0, 1).widget() + sample_name._main_widget.setText("Test Sample") + scan_control._metadata_form._additional_metadata._table_model._data = TEST_TABLE_ENTRY + scan_control._metadata_form.validate_form() + + assert scan_control._scan_metadata == TEST_MD + + scans = SimpleNamespace(grid_scan=MagicMock()) + with ( + patch.object(scan_control, "scans", scans), + patch.object(scan_control, "get_scan_parameters", lambda: ((), {})), + ): + scan_control.run_scan() + scans.grid_scan.assert_called_once_with(metadata=TEST_MD) def test_restore_parameters_with_fewer_arg_bundles(scan_control, qtbot):