Skip to content

Commit

Permalink
added 'save as' function
Browse files Browse the repository at this point in the history
  • Loading branch information
alexhroom committed Sep 16, 2024
1 parent 4d64ad4 commit 608b147
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 8 deletions.
24 changes: 24 additions & 0 deletions rascal2/dialogs/confirm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Confirmation dialog."""

from PyQt6 import QtWidgets


class ConfirmDialog(QtWidgets.QDialog):
"""Dialog to confirm an action."""

def __init__(self, title, text, parent=None):
super().__init__(parent)

self.setWindowTitle(title)

button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)

layout = QtWidgets.QVBoxLayout()
message = QtWidgets.QLabel(text)
layout.addWidget(message)
layout.addWidget(button_box)
self.setLayout(layout)
2 changes: 1 addition & 1 deletion rascal2/ui/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ def load_r1_project(self, load_path: str):
"""
self.project = RAT.utils.convert.r1_to_project_class(load_path)
self.controls = RAT.Controls()
self.save_path = Path(load_path).parent
self.save_path = str(Path(load_path).parent)
14 changes: 14 additions & 0 deletions rascal2/ui/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ def edit_controls(self, setting: str, value: Any):
self.view.undo_stack.push(commands.EditControls(self.model.controls, setting, value))
return True

def save_project(self, to_path: str | None = None):
"""Save the model.
Parameters
----------
to_path : str or None
If not None, save the model to the specified folder.
"""
if to_path is not None:
self.model.save_path = to_path

self.model.save_project()

def interrupt_terminal(self):
"""Sends an interrupt signal to the terminal."""
# TODO: stub for when issue #9 is resolved
Expand Down
35 changes: 30 additions & 5 deletions rascal2/ui/view.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import pathlib
from pathlib import Path
from typing import Literal

from PyQt6 import QtCore, QtGui, QtWidgets

from rascal2.config import path_for, setup_logging, setup_settings
from rascal2.core.settings import MDIGeometries
from rascal2.dialogs.project_dialog import LoadDialog, LoadR1Dialog, NewProjectDialog
from rascal2.dialogs.confirm import ConfirmDialog
from rascal2.dialogs.project_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog
from rascal2.widgets import ControlsWidget
from rascal2.widgets.startup_widget import StartUpWidget

Expand Down Expand Up @@ -91,9 +92,15 @@ def create_actions(self):
self.save_project_action = QtGui.QAction("&Save", self)
self.save_project_action.setStatusTip("Save project")
self.save_project_action.setIcon(QtGui.QIcon(path_for("save-project.png")))
self.save_project_action.triggered.connect(self.presenter.model.save_project)
self.save_project_action.triggered.connect(self.presenter.save_project)
self.save_project_action.setShortcut(QtGui.QKeySequence.StandardKey.Save)

self.save_as_action = QtGui.QAction("Save To &Folder...", self)
self.save_as_action.setStatusTip("Save project to a specified folder.")
self.save_as_action.setIcon(QtGui.QIcon(path_for("save-project.png")))
self.save_as_action.triggered.connect(self.save_as)
self.save_as_action.setShortcut(QtGui.QKeySequence.StandardKey.SaveAs)

self.undo_action = QtGui.QAction("&Undo", self)
self.undo_action.setStatusTip("Undo")
self.undo_action.setIcon(QtGui.QIcon(path_for("undo.png")))
Expand Down Expand Up @@ -142,6 +149,7 @@ def create_menus(self):
file_menu.addAction(self.open_r1_action)
file_menu.addSeparator()
file_menu.addAction(self.save_project_action)
file_menu.addAction(self.save_as_action)
file_menu.addSeparator()
file_menu.addAction(self.exit_action)

Expand Down Expand Up @@ -248,11 +256,28 @@ def init_settings_and_log(self, save_path: str):
The save path for the project.
"""
proj_path = pathlib.Path(save_path)
proj_path = Path(save_path)
self.settings = setup_settings(proj_path)
log_path = pathlib.Path(self.settings.log_path)
log_path = Path(self.settings.log_path)
if not log_path.is_absolute():
log_path = proj_path / log_path

log_path.parents[0].mkdir(parents=True, exist_ok=True)
self.logging = setup_logging(log_path, level=self.settings.log_level)

def save_as(self):
"""Save a project to a specified folder."""
project_folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Folder")
if project_folder:
if any(Path(project_folder, file).exists() for file in PROJECT_FILES):
overwrite_dlg = ConfirmDialog(
title="Confirm Overwrite",
text="A project already exists in this folder, do you want to replace it?",
parent=self,
)
if not overwrite_dlg.exec():
# return to file selection
self.save_as()
return

self.presenter.save_project(to_path=project_folder)
10 changes: 10 additions & 0 deletions tests/dialogs/test_confirm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Test the confirm dialog."""

from rascal2.dialogs.confirm import ConfirmDialog


def test_confirm_init():
"""Test that we can create a confirmation dialog."""
dlg = ConfirmDialog("Confirm Title", "Confirm?")
assert dlg.windowTitle() == "Confirm Title"
assert dlg.layout().itemAt(0).widget().text() == "Confirm?"
15 changes: 14 additions & 1 deletion tests/test_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_controls_validation_error(presenter, param, value):


@pytest.mark.parametrize("function", ["create_project", "load_project", "load_r1_project"])
def test_loadt_project(presenter, function):
def test_load_project(presenter, function):
"""All the project initialisation functions should run the corresponding model function and initialise UI."""
presenter.initialise_ui = MagicMock()
setattr(presenter.model, function, MagicMock())
Expand All @@ -75,3 +75,16 @@ def test_loadt_project(presenter, function):

presenter.initialise_ui.assert_called_once_with("proj_name", "some_path/")
getattr(presenter.model, function).assert_called_once_with(*params)


def test_save_project(presenter):
"""Test that projects can be saved, optionally saved as a new folder."""
presenter.model.save_project = MagicMock()
presenter.save_project()
presenter.model.save_project.assert_called_once()

presenter.model.save_project.reset_mock()

presenter.save_project(to_path="new path/")
assert presenter.model.save_path == "new path/"
presenter.model.save_project.assert_called_once()
47 changes: 46 additions & 1 deletion tests/test_view.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Unit tests for the main window view."""

from unittest.mock import patch
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch

import pytest

Expand Down Expand Up @@ -62,3 +64,46 @@ def test_set_mdi(self, mock1, mock2, geometry):
window.height(),
window.isMinimized(),
)


@patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory")
def test_save_as(mock_get_dir: MagicMock):
"""Test that saving to a specified folder works as expected."""
view = MainWindowView()
save_mock = view.presenter.save_project = MagicMock()
mock_overwrite = MagicMock(return_value=True)

with TemporaryDirectory() as tmp:
view.presenter.create_project("test", tmp)
mock_get_dir.return_value = tmp

with patch("rascal2.ui.view.ConfirmDialog.exec", new=mock_overwrite):
view.save_as()

save_mock.assert_called_once()

save_mock.reset_mock()

# check overwrite is triggered if project already in folder
Path(tmp, "controls.json").touch()
with patch("rascal2.ui.view.ConfirmDialog.exec", new=mock_overwrite):
view.save_as()
mock_overwrite.assert_called_once()
save_mock.assert_called_once()

save_mock.reset_mock()

def change_dir():
"""Change directory so mocked save_as doesn't recurse forever."""
mock_get_dir.return_value = "OTHERPATH"

# check not saved if overwrite is cancelled
# to avoid infinite recursion (which only happens because of the mock),
# set the mock to change the directory to some other path once called
mock_overwrite = MagicMock(return_value=False, side_effect=change_dir)

with patch("rascal2.ui.view.ConfirmDialog.exec", new=mock_overwrite):
view.save_as()

mock_overwrite.assert_called_once()
save_mock.assert_called_once_with(to_path="OTHERPATH")

0 comments on commit 608b147

Please sign in to comment.