diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/ui/lib/filedialog.py new file mode 100755 index 000000000..8159078e0 --- /dev/null +++ b/openlp/core/ui/lib/filedialog.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" Patch the QFileDialog so it accepts and returns Path objects""" +from functools import wraps +from pathlib import Path + +from PyQt5 import QtWidgets + +from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.lib import replace_params + + +class FileDialog(QtWidgets.QFileDialog): + @classmethod + @wraps(QtWidgets.QFileDialog.getExistingDirectory) + def getExistingDirectory(cls, *args, **kwargs): + """ + Reimplement `getExistingDirectory` so that it can be called with, and return Path objects + + :type parent: QtWidgets.QWidget or None + :type caption: str + :type directory: pathlib.Path + :type options: QtWidgets.QFileDialog.Options + :rtype: tuple[Path, str] + """ + args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) + + return_value = super().getExistingDirectory(*args, **kwargs) + + # getExistingDirectory returns a str that represents the path. The string is empty if the user cancels the + # dialog. + return str_to_path(return_value) + + @classmethod + @wraps(QtWidgets.QFileDialog.getOpenFileName) + def getOpenFileName(cls, *args, **kwargs): + """ + Reimplement `getOpenFileName` so that it can be called with, and return Path objects + + :type parent: QtWidgets.QWidget or None + :type caption: str + :type directory: pathlib.Path + :type filter: str + :type initialFilter: str + :type options: QtWidgets.QFileDialog.Options + :rtype: tuple[Path, str] + """ + args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) + + file_name, selected_filter = super().getOpenFileName(*args, **kwargs) + + # getOpenFileName returns a tuple. The first item is a str that represents the path. The string is empty if + # the user cancels the dialog. + return str_to_path(file_name), selected_filter + + @classmethod + def getOpenFileNames(cls, *args, **kwargs): + """ + Reimplement `getOpenFileNames` so that it can be called with, and return Path objects + + :type parent: QtWidgets.QWidget or None + :type caption: str + :type directory: pathlib.Path + :type filter: str + :type initialFilter: str + :type options: QtWidgets.QFileDialog.Options + :rtype: tuple[list[Path], str] + """ + args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) + + file_names, selected_filter = super().getOpenFileNames(*args, **kwargs) + + # getSaveFileName returns a tuple. The first item is a list of str's that represents the path. The list is + # empty if the user cancels the dialog. + paths = [str_to_path(path) for path in file_names] + return paths, selected_filter + + @classmethod + def getSaveFileName(cls, *args, **kwargs): + """ + Reimplement `getSaveFileName` so that it can be called with, and return Path objects + + :type parent: QtWidgets.QWidget or None + :type caption: str + :type directory: pathlib.Path + :type filter: str + :type initialFilter: str + :type options: QtWidgets.QFileDialog.Options + :rtype: tuple[Path or None, str] + """ + args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) + + file_name, selected_filter = super().getSaveFileName(*args, **kwargs) + + # getSaveFileName returns a tuple. The first item represents the path as a str. The string is empty if the user + # cancels the dialog. + return str_to_path(file_name), selected_filter diff --git a/tests/functional/openlp_core_ui_lib/test_filedialog.py b/tests/functional/openlp_core_ui_lib/test_filedialog.py new file mode 100755 index 000000000..6ec045d47 --- /dev/null +++ b/tests/functional/openlp_core_ui_lib/test_filedialog.py @@ -0,0 +1,188 @@ +import os +from unittest import TestCase +from unittest.mock import patch +from pathlib import Path + +from PyQt5 import QtWidgets + +from openlp.core.ui.lib.filedialog import FileDialog + + +class TestFileDialogPatches(TestCase): + """ + Tests for the :mod:`openlp.core.ui.lib.filedialogpatches` module + """ + + def test_file_dialog(self): + """ + Test that the :class:`FileDialog` instantiates correctly + """ + # GIVEN: The FileDialog class + # WHEN: Creating an instance + instance = FileDialog() + + # THEN: The instance should be an instance of QFileDialog + self.assertIsInstance(instance, QtWidgets.QFileDialog) + + def test_get_existing_directory_user_abort(self): + """ + Test that `getExistingDirectory` handles the case when the user cancels the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getExistingDirectory method + # WHEN: Calling FileDialog.getExistingDirectory and the user cancels the dialog returns a empty string + with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=''): + result = FileDialog.getExistingDirectory() + + # THEN: The result should be None + self.assertEqual(result, None) + + def test_get_existing_directory_user_accepts(self): + """ + Test that `getExistingDirectory` handles the case when the user accepts the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getExistingDirectory method + # WHEN: Calling FileDialog.getExistingDirectory, the user chooses a file and accepts the dialog (it returns a + # string pointing to the directory) + with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=os.path.join('test', 'dir')): + result = FileDialog.getExistingDirectory() + + # THEN: getExistingDirectory() should return a Path object pointing to the chosen file + self.assertEqual(result, Path('test', 'dir')) + + def test_get_existing_directory_param_order(self): + """ + Test that `getExistingDirectory` passes the parameters to `QFileDialog.getExistingDirectory` in the correct + order + """ + # GIVEN: FileDialog + with patch('openlp.core.ui.lib.filedialog.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \ + as mocked_get_existing_directory: + + # WHEN: Calling the getExistingDirectory method with all parameters set + FileDialog.getExistingDirectory('Parent', 'Caption', Path('test', 'dir'), 'Options') + + # THEN: The `QFileDialog.getExistingDirectory` should have been called with the parameters in the correct + # order + mocked_get_existing_directory.assert_called_once_with('Parent', 'Caption', os.path.join('test', 'dir'), + 'Options') + + def test_get_open_file_name_user_abort(self): + """ + Test that `getOpenFileName` handles the case when the user cancels the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getOpenFileName method + # WHEN: Calling FileDialog.getOpenFileName and the user cancels the dialog (it returns a tuple with the first + # value set as an empty string) + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')): + result = FileDialog.getOpenFileName() + + # THEN: First value should be None + self.assertEqual(result[0], None) + + def test_get_open_file_name_user_accepts(self): + """ + Test that `getOpenFileName` handles the case when the user accepts the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getOpenFileName method + # WHEN: Calling FileDialog.getOpenFileName, the user chooses a file and accepts the dialog (it returns a + # tuple with the first value set as an string pointing to the file) + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', + return_value=(os.path.join('test', 'chosen.file'), '')): + result = FileDialog.getOpenFileName() + + # THEN: getOpenFileName() should return a tuple with the first value set to a Path object pointing to the + # chosen file + self.assertEqual(result[0], Path('test', 'chosen.file')) + + def test_get_open_file_name_selected_filter(self): + """ + Test that `getOpenFileName` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileName` + """ + # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method + # WHEN: Calling FileDialog.getOpenFileName, and `QFileDialog.getOpenFileName` returns a known `selectedFilter` + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', 'selected filter')): + result = FileDialog.getOpenFileName() + + # THEN: getOpenFileName() should return a tuple with the second value set to a the selected filter + self.assertEqual(result[1], 'selected filter') + + def test_get_open_file_names_user_abort(self): + """ + Test that `getOpenFileNames` handles the case when the user cancels the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method + # WHEN: Calling FileDialog.getOpenFileNames and the user cancels the dialog (it returns a tuple with the first + # value set as an empty list) + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], '')): + result = FileDialog.getOpenFileNames() + + # THEN: First value should be an empty list + self.assertEqual(result[0], []) + + def test_get_open_file_names_user_accepts(self): + """ + Test that `getOpenFileNames` handles the case when the user accepts the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method + # WHEN: Calling FileDialog.getOpenFileNames, the user chooses some files and accepts the dialog (it returns a + # tuple with the first value set as a list of strings pointing to the file) + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', + return_value=([os.path.join('test', 'chosen.file1'), os.path.join('test', 'chosen.file2')], '')): + result = FileDialog.getOpenFileNames() + + # THEN: getOpenFileNames() should return a tuple with the first value set to a list of Path objects pointing + # to the chosen file + self.assertEqual(result[0], [Path('test', 'chosen.file1'), Path('test', 'chosen.file2')]) + + def test_get_open_file_names_selected_filter(self): + """ + Test that `getOpenFileNames` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileNames` + """ + # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method + # WHEN: Calling FileDialog.getOpenFileNames, and `QFileDialog.getOpenFileNames` returns a known + # `selectedFilter` + with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], 'selected filter')): + result = FileDialog.getOpenFileNames() + + # THEN: getOpenFileNames() should return a tuple with the second value set to a the selected filter + self.assertEqual(result[1], 'selected filter') + + def test_get_save_file_name_user_abort(self): + """ + Test that `getSaveFileName` handles the case when the user cancels the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method + # WHEN: Calling FileDialog.getSaveFileName and the user cancels the dialog (it returns a tuple with the first + # value set as an empty string) + with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', '')): + result = FileDialog.getSaveFileName() + + # THEN: First value should be None + self.assertEqual(result[0], None) + + def test_get_save_file_name_user_accepts(self): + """ + Test that `getSaveFileName` handles the case when the user accepts the dialog + """ + # GIVEN: FileDialog with a mocked QDialog.getSaveFileName method + # WHEN: Calling FileDialog.getSaveFileName, the user chooses a file and accepts the dialog (it returns a + # tuple with the first value set as an string pointing to the file) + with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', + return_value=(os.path.join('test', 'chosen.file'), '')): + result = FileDialog.getSaveFileName() + + # THEN: getSaveFileName() should return a tuple with the first value set to a Path object pointing to the + # chosen file + self.assertEqual(result[0], Path('test', 'chosen.file')) + + def test_get_save_file_name_selected_filter(self): + """ + Test that `getSaveFileName` does not modify the selectedFilter as returned by `QFileDialog.getSaveFileName` + """ + # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method + # WHEN: Calling FileDialog.getSaveFileName, and `QFileDialog.getSaveFileName` returns a known `selectedFilter` + with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', 'selected filter')): + result = FileDialog.getSaveFileName() + + # THEN: getSaveFileName() should return a tuple with the second value set to a the selected filter + self.assertEqual(result[1], 'selected filter')