diff --git a/rascal2/ui/model.py b/rascal2/ui/model.py index 56b581a7..8f0e6235 100644 --- a/rascal2/ui/model.py +++ b/rascal2/ui/model.py @@ -149,6 +149,16 @@ def save_project(self, save_path): self.save_path = save_path os.chdir(save_path) + def save_project_as_script(self, save_path): + """Save the project to the save path as a script file. + + Parameters + ---------- + save_path : str + The save path of the project. + """ + self.project.write_script(script=save_path) + def is_project_example(self): return Path(self.save_path).is_relative_to(EXAMPLES_TEMP_PATH) diff --git a/rascal2/ui/presenter.py b/rascal2/ui/presenter.py index 5e88b36d..6f654463 100644 --- a/rascal2/ui/presenter.py +++ b/rascal2/ui/presenter.py @@ -106,7 +106,7 @@ def edit_controls(self, setting: str, value: Any): self.model.controls.model_validate({setting: value}) self.view.undo_stack.push(commands.EditControls({setting: value}, self)) - def save_project(self, save_as: bool = False): + def save_project(self, save_as: bool = False, as_script: bool = False): """Save the model. Parameters @@ -114,6 +114,9 @@ def save_project(self, save_as: bool = False): save_as : bool Whether we are saving to the existing save path or to a specified folder. + as_script: bool + Whether we are saving the project as a script or not. + Returns ------- : bool @@ -132,7 +135,14 @@ def save_project(self, save_as: bool = False): if not to_path: return False try: - self.model.save_project(to_path) + if as_script: + filename = self.model.project.name.replace(" ", "_") + save_file = self.view.get_save_file("Save Project as Script", filename, "*.py") + if not save_file: + return + self.model.save_project_as_script(save_file) + else: + self.model.save_project(to_path) except OSError as err: LOGGER.error(f"Failed to save project to {to_path}.\n", exc_info=err) else: diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 2420abe0..0718f652 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -124,7 +124,15 @@ def create_actions(self): self.save_as_action.setIcon(QtGui.QIcon(path_for("save-project.png"))) self.save_as_action.triggered.connect(lambda: self.presenter.save_project(save_as=True)) self.save_as_action.setShortcut(QtGui.QKeySequence.StandardKey.SaveAs) - self.disabled_elements.append(self.save_project_action) + self.save_as_action.setEnabled(False) + self.disabled_elements.append(self.save_as_action) + + self.save_as_script_action = QtGui.QAction("Save Project as &Script...", self) + self.save_as_script_action.setStatusTip("Save project as a script.") + self.save_as_script_action.setIcon(QtGui.QIcon(path_for("save-project.png"))) + self.save_as_script_action.triggered.connect(lambda: self.presenter.save_project(as_script=True)) + self.save_as_script_action.setEnabled(False) + self.disabled_elements.append(self.save_as_script_action) self.undo_action = self.undo_stack.createUndoAction(self, "&Undo") self.undo_action.setStatusTip("Undo the last action") @@ -217,6 +225,7 @@ def create_menus(self): file_menu.addSeparator() file_menu.addAction(self.save_project_action) file_menu.addAction(self.save_as_action) + file_menu.addAction(self.save_as_script_action) file_menu.addSeparator() file_menu.addAction(self.export_fits_action) file_menu.addSeparator() diff --git a/tests/ui/test_model.py b/tests/ui/test_model.py index da38cf4b..dbf15839 100644 --- a/tests/ui/test_model.py +++ b/tests/ui/test_model.py @@ -77,6 +77,20 @@ def test_save_project(empty_results, model): assert '"fitParams": []' in results +def test_save_project_as_script(model): + model.project = Project(calculation="domains", name="test project") + with TemporaryDirectory() as tmpdir: + model.save_project_as_script(tmpdir + "/test_script.py") + + script = Path(tmpdir, "test_script.py").read_text() + + assert 'name="test project"' in script + assert 'calculation="domains"' in script + assert 'model="standard layers"' in script + assert 'geometry="air/substrate"' in script + assert 'absorption="False"' in script + + def test_load_project(empty_results, model): """The load function should load the correct controls object from JSON.""" project = Project(name="test project", calculation="domains") diff --git a/tests/ui/test_presenter.py b/tests/ui/test_presenter.py index 3f42fe32..b51c1bb0 100644 --- a/tests/ui/test_presenter.py +++ b/tests/ui/test_presenter.py @@ -219,6 +219,17 @@ def test_save_project(recent_projects_mock, presenter): recent_projects_mock.assert_called_with("new path/") +def test_save_project_as_script(presenter): + """Test that projects can be saved as a script, optionally saved as a new folder.""" + presenter.model.project = MagicMock() + presenter.model.project.name = "test_name" + presenter.model.controls = MagicMock() + presenter.view.project_widget.stacked_widget.currentIndex = MagicMock(return_value=0) + presenter.view.get_save_file = MagicMock(return_value="test_name.py") + presenter.save_project(as_script=True) + presenter.model.project.write_script.assert_called_once_with(script="test_name.py") + + @pytest.mark.parametrize( ["reply", "undo_clean_state", "expected"], [ diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 7bb77c52..e9754f8a 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -177,6 +177,7 @@ def test_menu_element_present(test_view, submenu_name): "", "&Save", "Save To &Folder...", + "Save Project as &Script...", "", "Export Fits", "",