From 9cb2b2e3c2774a609a31a31efc503d5b628998f7 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Tue, 5 Sep 2017 21:48:55 +0100 Subject: [PATCH 01/52] Pathlib changes --- openlp/core/__init__.py | 42 +++-- openlp/core/common/httputils.py | 46 +++--- openlp/core/lib/shutil.py | 97 +++++++++++ openlp/core/ui/firsttimeform.py | 6 +- openlp/plugins/remotes/deploy.py | 2 +- .../openlp_core_common/test_httputils.py | 3 +- .../functional/openlp_core_lib/test_shutil.py | 151 ++++++++++++++++++ 7 files changed, 294 insertions(+), 53 deletions(-) create mode 100755 openlp/core/lib/shutil.py create mode 100755 tests/functional/openlp_core_lib/test_shutil.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 88b3dbfb7..0fcea2d1a 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -29,8 +29,6 @@ logging and a plugin framework are contained within the openlp.core module. import argparse import logging -import os -import shutil import sys import time from datetime import datetime @@ -43,6 +41,7 @@ from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManag from openlp.core.common.path import Path from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList +from openlp.core.lib.shutil import copytree from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen from openlp.core.ui.exceptionform import ExceptionForm @@ -181,25 +180,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): """ Check if the data folder path exists. """ - data_folder_path = str(AppLocation.get_data_path()) - if not os.path.exists(data_folder_path): - log.critical('Database was not found in: ' + data_folder_path) - status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'), - translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}' - '\n\nThe location of the data folder was ' - 'previously changed from the OpenLP\'s ' - 'default location. If the data was stored on ' - 'removable device, that device needs to be ' - 'made available.\n\nYou may reset the data ' - 'location back to the default location, ' - 'or you can try to make the current location ' - 'available.\n\nDo you want to reset to the ' - 'default data location? If not, OpenLP will be ' - 'closed so you can try to fix the the problem.') - .format(path=data_folder_path), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No), - QtWidgets.QMessageBox.No) + data_folder_path = AppLocation.get_data_path() + if not data_folder_path.exists(): + log.critical('Database was not found in: %s', data_folder_path) + status = QtWidgets.QMessageBox.critical( + None, translate('OpenLP', 'Data Directory Error'), + translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data folder ' + 'was previously changed from the OpenLP\'s default location. If the data was ' + 'stored on removable device, that device needs to be made available.\n\nYou may ' + 'reset the data location back to the default location, or you can try to make the ' + 'current location available.\n\nDo you want to reset to the default data location? ' + 'If not, OpenLP will be closed so you can try to fix the the problem.') + .format(path=data_folder_path), + QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), + QtWidgets.QMessageBox.No) if status == QtWidgets.QMessageBox.No: # If answer was "No", return "True", it will shutdown OpenLP in def main log.info('User requested termination') @@ -253,11 +247,11 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): 'a backup of the old data folder?'), defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: # Create copy of data folder - data_folder_path = str(AppLocation.get_data_path()) + data_folder_path = AppLocation.get_data_path() timestamp = time.strftime("%Y%m%d-%H%M%S") - data_folder_backup_path = data_folder_path + '-' + timestamp + data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp) try: - shutil.copytree(data_folder_path, data_folder_backup_path) + copytree(data_folder_path, data_folder_backup_path) except OSError: QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'), translate('OpenLP', 'Backup of the data folder failed!')) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index b0c9c1b2f..9f95f4924 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -211,38 +211,32 @@ def url_get_file(callback, url, f_path, sha256=None): :param callback: the class which needs to be updated :param url: URL to download - :param f_path: Destination file + :param openlp.core.common.path.Path f_path: Destination file :param sha256: The check sum value to be checked against the download value """ block_count = 0 block_size = 4096 retries = 0 log.debug("url_get_file: " + url) + if sha256: + hasher = hashlib.sha256() while True: try: - filename = open(f_path, "wb") - url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - if sha256: - hasher = hashlib.sha256() - # Download until finished or canceled. - while not callback.was_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) - if sha256: - hasher.update(data) - block_count += 1 - callback._download_progress(block_count, block_size) - filename.close() - if sha256 and hasher.hexdigest() != sha256: - log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) - os.remove(f_path) - return False - except (urllib.error.URLError, socket.timeout) as err: + with f_path.open('wb') as file: + url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + # Download until finished or canceled. + while not callback.was_cancelled: + data = url_file.read(block_size) + if not data: + break + file.write(data) + if sha256: + hasher.update(data) + block_count += 1 + callback._download_progress(block_count, block_size) + except (urllib.error.URLError, socket.timeout): trace_error_handler(log) - filename.close() - os.remove(f_path) + f_path.unlink() if retries > CONNECTION_RETRIES: return False else: @@ -251,8 +245,12 @@ def url_get_file(callback, url, f_path, sha256=None): continue break # Delete file if cancelled, it may be a partial file. + if sha256 and hasher.hexdigest() != sha256: + log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) + f_path.unlink() + return False if callback.was_cancelled: - os.remove(f_path) + f_path.unlink() return True diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py new file mode 100755 index 000000000..d2f6ae34d --- /dev/null +++ b/openlp/core/lib/shutil.py @@ -0,0 +1,97 @@ +# -*- 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 shutil methods we use so they accept and return Path objects""" +import shutil + +from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.lib import replace_params + + +def copy(*args, **kwargs): + """ + Wraps :func:`shutil.copy` so that we can accept Path objects. + + :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copy + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copy(*args, **kwargs)) + + +def copyfile(*args, **kwargs): + """ + Wraps :func:`shutil.copyfile` so that we can accept Path objects. + + :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copyfile + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copyfile(*args, **kwargs)) + + +def copytree(*args, **kwargs): + """ + Wraps :func:shutil.copytree` so that we can accept Path objects. + + :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copytree(*args, **kwargs)) + + +def rmtree(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) + + return shutil.rmtree(*args, **kwargs) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 1ae923467..133d7d65b 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -563,7 +563,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): filename, sha256 = item.data(QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading.format(name=filename), 0) self.previous_size = 0 - destination = os.path.join(songs_destination, str(filename)) + destination = Path(songs_destination, str(filename)) if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), destination, sha256): missed_files.append('Song: {name}'.format(name=filename)) @@ -576,7 +576,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), - os.path.join(bibles_destination, bible), + Path(bibles_destination, bible), sha256): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 @@ -588,7 +588,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), - os.path.join(themes_destination, theme), + Path(themes_destination, theme), sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: diff --git a/openlp/plugins/remotes/deploy.py b/openlp/plugins/remotes/deploy.py index d971499f0..8706bc011 100644 --- a/openlp/plugins/remotes/deploy.py +++ b/openlp/plugins/remotes/deploy.py @@ -64,6 +64,6 @@ def download_and_check(callback=None): file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') callback.setRange(0, file_size) if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), - os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'), + AppLocation.get_section_data_path('remotes') / 'site.zip', sha256=sha256): deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 26ac297dd..03dde026c 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -29,6 +29,7 @@ from unittest import TestCase from unittest.mock import MagicMock, patch from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping +from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin @@ -267,7 +268,7 @@ class TestHttpUtils(TestCase, TestMixin): mocked_urlopen.side_effect = socket.timeout() # WHEN: Attempt to retrieve a file - url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile) + url_get_file(MagicMock(), url='http://localhost/test', f_path=Path(self.tempfile)) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py new file mode 100755 index 000000000..f502e403b --- /dev/null +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -0,0 +1,151 @@ +import os +from unittest import TestCase +from unittest.mock import ANY, MagicMock, patch + +from openlp.core.common.path import Path +from openlp.core.lib import shutilpatches + + +class TestShutilPatches(TestCase): + """ + Tests for the :mod:`openlp.core.lib.shutil` module + """ + + def test_pcopy(self): + """ + Test :func:`copy` + """ + # GIVEN: A mocked `shutil.copy` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copy: + + # WHEN: Calling shutilpatches.copy with the src and dst parameters as Path object types + result = shutilpatches.copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copy` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copy` should return the str type result of calling `shutil.copy` as a Path + # object. + mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopy_follow_optional_params(self): + """ + Test :func:`copy` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copy` + with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: + + # WHEN: Calling shutilpatches.copy with `follow_symlinks` set to False + shutilpatches.copy(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + follow_symlinks=False) + + # THEN: `shutil.copy` should have been called with follow_symlinks is set to false + mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_pcopyfile(self): + """ + Test :func:`copyfile` + """ + # GIVEN: A mocked `shutil.copyfile` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copyfile', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copyfile: + + # WHEN: Calling shutilpatches.copyfile with the src and dst parameters as Path object types + result = shutilpatches.copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copyfile` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copyfile` should return the str type result of calling `shutil.copyfile` as a Path + # object. + mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopyfile_optional_params(self): + """ + Test :func:`copyfile` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copyfile` + with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: + + # WHEN: Calling shutilpatches.copyfile with `follow_symlinks` set to False + shutilpatches.copyfile(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + follow_symlinks=False) + + # THEN: `shutil.copyfile` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_pcopytree(self): + """ + Test :func:`copytree` + """ + # GIVEN: A mocked `shutil.copytree` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copytree', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copytree: + + # WHEN: Calling shutilpatches.copytree with the src and dst parameters as Path object types + result = shutilpatches.copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copytree` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copytree` should return the str type result of calling `shutil.copytree` as a Path + # object. + mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopytree_optional_params(self): + """ + Test :func:`copytree` when optional parameters are passed + """ + # GIVEN: A mocked `shutil.copytree` + with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: + mocked_ignore = MagicMock() + mocked_copy_function = MagicMock() + + # WHEN: Calling shutilpatches.copytree with the optional parameters set + shutilpatches.copytree(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + symlinks=True, + ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + # THEN: `shutil.copytree` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, + symlinks=True, + ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + def test_prmtree(self): + """ + Test :func:`rmtree` + """ + # GIVEN: A mocked `shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_rmtree: + + # WHEN: Calling shutilpatches.rmtree with the path parameter as Path object type + result = shutilpatches.rmtree(Path('test', 'path')) + + # THEN: `shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_rmtree.assert_called_once_with(os.path.join('test', 'path')) + self.assertIsNone(result) + + def test_prmtree_optional_params(self): + """ + Test :func:`rmtree` when optional parameters are passed + """ + # GIVEN: A mocked `shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: + mocked_on_error = MagicMock() + + # WHEN: Calling shutilpatches.rmtree with `ignore_errors` set to True and `onerror` set to a mocked object + shutilpatches.rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + + # THEN: `shutil.rmtree` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) From 292861907ef944e14bf620c52997b118cf9c056f Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 6 Sep 2017 21:18:08 +0100 Subject: [PATCH 02/52] minor edits --- openlp/core/common/applocation.py | 1 - openlp/core/common/httputils.py | 4 ++-- openlp/core/common/languagemanager.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 87ef7e6c1..02a872303 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -29,7 +29,6 @@ import sys from openlp.core.common import Settings, is_win, is_macosx from openlp.core.common.path import Path - if not is_win() and not is_macosx(): try: from xdg import BaseDirectory diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 9f95f4924..90e128063 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -218,10 +218,10 @@ def url_get_file(callback, url, f_path, sha256=None): block_size = 4096 retries = 0 log.debug("url_get_file: " + url) - if sha256: - hasher = hashlib.sha256() while True: try: + if sha256: + hasher = hashlib.sha256() with f_path.open('wb') as file: url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) # Download until finished or canceled. diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index 35b195031..40e4930fb 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -141,7 +141,7 @@ class LanguageManager(object): if reg_ex.exactMatch(qmf): name = '{regex}'.format(regex=reg_ex.cap(1)) LanguageManager.__qm_list__[ - '{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name + '{count:>2d} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name @staticmethod def get_qm_list(): From 24358337e7c3e25d378828e302fc61da1e92fbae Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 6 Sep 2017 22:36:31 +0100 Subject: [PATCH 03/52] Pathlib changes --- openlp/core/common/registry.py | 5 +- openlp/core/common/uistrings.py | 3 -- openlp/core/ui/servicemanager.py | 37 ++++++------- openlp/plugins/songs/reporting.py | 88 ++++++++++++++----------------- 4 files changed, 59 insertions(+), 74 deletions(-) diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 95f1ce721..33afe6f21 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -138,12 +138,9 @@ class Registry(object): if result: results.append(result) except TypeError: - # Who has called me can help in debugging - trace_error_handler(log) log.exception('Exception for function {function}'.format(function=function)) else: - trace_error_handler(log) - log.error("Event {event} called but not registered".format(event=event)) + log.exception('Event {event} called but not registered'.format(event=event)) return results def get_flag(self, key): diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 02937351d..9dd24a866 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -88,9 +88,6 @@ class UiStrings(object): self.Error = translate('OpenLP.Ui', 'Error') self.Export = translate('OpenLP.Ui', 'Export') self.File = translate('OpenLP.Ui', 'File') - self.FileNotFound = translate('OpenLP.Ui', 'File Not Found') - self.FileNotFoundMessage = translate('OpenLP.Ui', - 'File {name} not found.\nPlease try selecting it individually.') self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit') self.Help = translate('OpenLP.Ui', 'Help') self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours') diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 5051e43c1..eb279f267 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -366,16 +366,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ return self._modified - def set_file_name(self, file_name): + def set_file_name(self, file_path): """ Setter for service file. - :param file_name: The service file name + :param openlp.core.common.path.Path file_path: The service file name + :rtype: None """ - self._file_name = str(file_name) + self._file_name = path_to_str(file_path) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) - Settings().setValue('servicemanager/last file', Path(file_name)) - self._save_lite = self._file_name.endswith('.oszl') + Settings().setValue('servicemanager/last file', file_path) + self._save_lite = file_path.suffix() == '.oszl' def file_name(self): """ @@ -474,7 +475,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ self.service_manager_list.clear() self.service_items = [] - self.set_file_name('') + self.set_file_name(None) self.service_id += 1 self.set_modified(False) Settings().setValue('servicemanager/last file', None) @@ -695,27 +696,23 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa default_file_name = format_time(default_pattern, local_time) else: default_file_name = '' - directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory')) - path = os.path.join(directory, default_file_name) + directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory') + file_path = directory_path / default_file_name # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # the long term. if self._file_name.endswith('oszl') or self.service_has_all_original_files: - file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, path, + file_path, filter_used = FileDialog.getSaveFileName( + self.main_window, UiStrings().SaveService, file_path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) else: - file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, path, + file_path, filter_used = FileDialog.getSaveFileName( + self.main_window, UiStrings().SaveService, file_path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) - if not file_name: + if not file_path: return False - if os.path.splitext(file_name)[1] == '': - file_name += '.osz' - else: - ext = os.path.splitext(file_name)[1] - file_name.replace(ext, '.osz') - self.set_file_name(file_name) + file_path.with_suffix('.osz') + self.set_file_name(file_path) self.decide_save_method() def decide_save_method(self, field=None): @@ -772,7 +769,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa return file_to.close() self.new_file() - self.set_file_name(file_name) + self.set_file_name(str_to_path(file_name)) self.main_window.display_progress_bar(len(items)) self.process_service_items(items) delete_file(Path(p_file)) diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index fc1a5f3f5..b50cd0a0c 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -25,10 +25,10 @@ The :mod:`db` module provides the ability to provide a csv file of all songs import csv import logging -from PyQt5 import QtWidgets - from openlp.core.common import Registry, translate +from openlp.core.common.path import Path from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.lib.filedialog import FileDialog from openlp.plugins.songs.lib.db import Song @@ -42,58 +42,55 @@ def report_song_list(): """ main_window = Registry().get('main_window') plugin = Registry().get('songs').plugin - report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( + report_file_path, filter_used = FileDialog.getSaveFileName( main_window, translate('SongPlugin.ReportSongList', 'Save File'), - translate('SongPlugin.ReportSongList', 'song_extract.csv'), + Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')), translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) - if not report_file_name: + if not report_file_path: main_window.error_message( translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), - translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your ' - 'report. \nPlease select an existing path ' - 'on your computer.') + translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n' + 'Please select an existing path on your computer.') ) return - if not report_file_name.endswith('csv'): - report_file_name += '.csv' - file_handle = None + report_file_path.with_suffix('.csv') Registry().get('application').set_busy_cursor() try: - file_handle = open(report_file_name, 'wt') - fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') - writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) - headers = dict((n, n) for n in fieldnames) - writer.writerow(headers) - song_list = plugin.manager.get_all_objects(Song) - for song in song_list: - author_list = [] - for author_song in song.authors_songs: - author_list.append(author_song.author.display_name) - author_string = ' | '.join(author_list) - book_list = [] - for book_song in song.songbook_entries: - if hasattr(book_song, 'entry') and book_song.entry: - book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry)) - book_string = ' | '.join(book_list) - topic_list = [] - for topic_song in song.topics: - if hasattr(topic_song, 'name'): - topic_list.append(topic_song.name) - topic_string = ' | '.join(topic_list) - writer.writerow({'Title': song.title, - 'Alternative Title': song.alternate_title, - 'Copyright': song.copyright, - 'Author(s)': author_string, - 'Song Book': book_string, - 'Topic': topic_string}) - Registry().get('application').set_normal_cursor() - main_window.information_message( - translate('SongPlugin.ReportSongList', 'Report Creation'), - translate('SongPlugin.ReportSongList', - 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name) - ) + with report_file_path.open('wt') as file_handle: + fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') + writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) + headers = dict((n, n) for n in fieldnames) + writer.writerow(headers) + song_list = plugin.manager.get_all_objects(Song) + for song in song_list: + author_list = [] + for author_song in song.authors_songs: + author_list.append(author_song.author.display_name) + author_string = ' | '.join(author_list) + book_list = [] + for book_song in song.songbook_entries: + if hasattr(book_song, 'entry') and book_song.entry: + book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry)) + book_string = ' | '.join(book_list) + topic_list = [] + for topic_song in song.topics: + if hasattr(topic_song, 'name'): + topic_list.append(topic_song.name) + topic_string = ' | '.join(topic_list) + writer.writerow({'Title': song.title, + 'Alternative Title': song.alternate_title, + 'Copyright': song.copyright, + 'Author(s)': author_string, + 'Song Book': book_string, + 'Topic': topic_string}) + Registry().get('application').set_normal_cursor() + main_window.information_message( + translate('SongPlugin.ReportSongList', 'Report Creation'), + translate('SongPlugin.ReportSongList', + 'Report \n{name} \nhas been successfully created. ').format(name=report_file_path) + ) except OSError as ose: Registry().get('application').set_normal_cursor() log.exception('Failed to write out song usage records') @@ -101,6 +98,3 @@ def report_song_list(): translate('SongPlugin.ReportSongList', 'An error occurred while extracting: {error}' ).format(error=ose.strerror)) - finally: - if file_handle: - file_handle.close() From f0e7381f5c107830f4d5a4164ca53206edb9436a Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 7 Sep 2017 22:52:39 +0100 Subject: [PATCH 04/52] Pathlib changes in presentation plugin --- openlp/core/ui/lib/wizard.py | 2 +- openlp/core/ui/mainwindow.py | 19 +++---- .../presentations/lib/impresscontroller.py | 15 +++--- openlp/plugins/presentations/lib/mediaitem.py | 14 +++--- .../presentations/lib/pdfcontroller.py | 34 ++++++------- .../presentations/lib/powerpointcontroller.py | 2 +- .../presentations/lib/pptviewcontroller.py | 12 ++--- .../lib/presentationcontroller.py | 50 +++++++++++-------- openlp/plugins/songs/reporting.py | 2 +- .../openlp_core_ui/test_exceptionform.py | 2 +- 10 files changed, 82 insertions(+), 70 deletions(-) diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 8f3093fef..677949b33 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -310,7 +310,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): """ folder_path = FileDialog.getExistingDirectory( self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), - QtWidgets.QFileDialog.ShowDirsOnly) + FileDialog.ShowDirsOnly) if folder_path: editbox.setText(str(folder_path)) Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 413e97a17..88799b060 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -42,6 +42,7 @@ from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon +from openlp.core.lib.shutil import copyfile from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController @@ -848,12 +849,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): QtWidgets.QMessageBox.No) if answer == QtWidgets.QMessageBox.No: return - import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName( + import_file_path, filter_used = FileDialog.getOpenFileName( self, translate('OpenLP.MainWindow', 'Import settings'), - '', + None, translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)')) - if not import_file_name: + if import_file_path is None: return setting_sections = [] # Add main sections. @@ -871,12 +872,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): # Add plugin sections. setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins]) # Copy the settings file to the tmp dir, because we do not want to change the original one. - temp_directory = os.path.join(str(gettempdir()), 'openlp') - check_directory_exists(Path(temp_directory)) - temp_config = os.path.join(temp_directory, os.path.basename(import_file_name)) - shutil.copyfile(import_file_name, temp_config) + temp_dir_path = Path(gettempdir(), 'openlp') + check_directory_exists(temp_dir_path) + temp_config_path = temp_dir_path / import_file_path.name + copyfile(import_file_path, temp_config_path) settings = Settings() - import_settings = Settings(temp_config, Settings.IniFormat) + import_settings = Settings(str(temp_config_path), Settings.IniFormat) log.info('hook upgrade_plugin_settings') self.plugin_manager.hook_upgrade_plugin_settings(import_settings) @@ -920,7 +921,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): settings.setValue('{key}'.format(key=section_key), value) now = datetime.now() settings.beginGroup(self.header_section) - settings.setValue('file_imported', import_file_name) + settings.setValue('file_imported', import_file_path) settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M")) settings.endGroup() settings.sync() diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 25c470f48..7699946a0 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -255,10 +255,10 @@ class ImpressDocument(PresentationDocument): if self.check_thumbnails(): return if is_win(): - thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \ + thumb_dir_url = 'file:///' + str(self.get_temp_folder()).replace('\\', '/') \ .replace(':', '|').replace(' ', '%20') else: - thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder()) + thumb_dir_url = uno.systemPathToFileUrl(str(self.get_temp_folder())) properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) @@ -266,17 +266,18 @@ class ImpressDocument(PresentationDocument): pages = doc.getDrawPages() if not pages: return - if not os.path.isdir(self.get_temp_folder()): - os.makedirs(self.get_temp_folder()) + temp_folder_path = self.get_temp_folder() + if not temp_folder_path.isdir(): + temp_folder_path.mkdir() for index in range(pages.getCount()): page = pages.getByIndex(index) doc.getCurrentController().setCurrentPage(page) url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') + path = temp_folder_path / '{number).png'.format(number=index + 1) try: doc.storeToURL(url_path, properties) - self.convert_thumbnail(path, index + 1) - delete_file(Path(path)) + self.convert_thumbnail(str(path), index + 1) + delete_file(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) except: diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 99c937eb0..275279e15 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -187,7 +187,7 @@ class PresentationMediaItem(MediaManagerItem): if controller_name: controller = self.controllers[controller_name] doc = controller.add_document(file) - thumb = os.path.join(doc.get_thumbnail_folder(), 'icon.png') + thumb = str(doc.get_thumbnail_folder() / 'icon.png') preview = doc.get_thumbnail_path(1, True) if not preview and not initial_load: doc.load_presentation() @@ -304,17 +304,17 @@ class PresentationMediaItem(MediaManagerItem): controller = self.controllers[processor] service_item.processor = None doc = controller.add_document(filename) - if doc.get_thumbnail_path(1, True) is None or not os.path.isfile( - os.path.join(doc.get_temp_folder(), 'mainslide001.png')): + if doc.get_thumbnail_path(1, True) is None or \ + not (doc.get_temp_folder() / 'mainslide001.png').is_file(): doc.load_presentation() i = 1 - image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) + image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) + thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) while os.path.isfile(image): service_item.add_from_image(image, name, thumbnail=thumbnail) i += 1 - image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide{number:d}.png'.format(number=i)) + image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) + thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index f4a091551..f80ad94ba 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -240,46 +240,46 @@ class PdfDocument(PresentationDocument): :return: True is loading succeeded, otherwise False. """ log.debug('load_presentation pdf') + temp_dir_path = self.get_temp_folder() # Check if the images has already been created, and if yes load them - if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')): - created_files = sorted(os.listdir(self.get_temp_folder())) - for fn in created_files: - if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): - self.image_files.append(os.path.join(self.get_temp_folder(), fn)) + if (temp_dir_path / 'mainslide001.png').is_file(): + created_files = sorted(temp_dir_path.glob('*')) + for image_path in created_files: + if image_path.is_file(): + self.image_files.append(str(image_path)) self.num_pages = len(self.image_files) return True size = ScreenList().current['size'] # Generate images from PDF that will fit the frame. runlog = '' try: - if not os.path.isdir(self.get_temp_folder()): - os.makedirs(self.get_temp_folder()) + if not temp_dir_path.is_dir(): + temp_dir_path.mkdir(parents=True) # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), - '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], + '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) elif self.controller.mutoolbin: log.debug('loading presentation using mutool') runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', str(size.height()), - '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], + '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) elif self.controller.gsbin: log.debug('loading presentation using gs') resolution = self.gs_get_resolution(size) runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', - '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), + '-sOutputFile=' + str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) - created_files = sorted(os.listdir(self.get_temp_folder())) - for fn in created_files: - if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): - self.image_files.append(os.path.join(self.get_temp_folder(), fn)) - except Exception as e: - log.debug(e) - log.debug(runlog) + created_files = sorted(temp_dir_path.glob('*')) + for image_path in created_files: + if image_path.is_file(): + self.image_files.append(str(image_path)) + except Exception: + log.exception(runlog) return False self.num_pages = len(self.image_files) # Create thumbnails diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 3bd726027..1014db851 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -177,7 +177,7 @@ class PowerpointDocument(PresentationDocument): if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden: self.index_map[key] = num + 1 self.presentation.Slides(num + 1).Export( - os.path.join(self.get_thumbnail_folder(), 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) + str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) key += 1 self.slide_count = key - 1 diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 645bef053..c936fe65c 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -121,17 +121,17 @@ class PptviewDocument(PresentationDocument): the background PptView task started earlier. """ log.debug('LoadPresentation') - temp_folder = self.get_temp_folder() + temp_dir_path = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) self.file_path = os.path.normpath(self.file_path) - preview_path = os.path.join(temp_folder, 'slide') + preview_path = temp_dir_path / 'slide' # Ensure that the paths are null terminated byte_file_path = self.file_path.encode('utf-16-le') + b'\0' - preview_path = preview_path.encode('utf-16-le') + b'\0' - if not os.path.isdir(temp_folder): - os.makedirs(temp_folder) - self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path) + preview_file_name = str(preview_path).encode('utf-16-le') + b'\0' + if not temp_dir_path: + temp_dir_path.mkdir(parents=True) + self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_file_name) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index e5f8ccf21..af665bb55 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -29,6 +29,7 @@ from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash from openlp.core.common.path import Path from openlp.core.lib import create_thumb, validate_thumb +from openlp.core.lib.shutil import rmtree log = logging.getLogger(__name__) @@ -99,7 +100,7 @@ class PresentationDocument(object): """ self.slide_number = 0 self.file_path = name - check_directory_exists(Path(self.get_thumbnail_folder())) + check_directory_exists(self.get_thumbnail_folder()) def load_presentation(self): """ @@ -116,10 +117,12 @@ class PresentationDocument(object): a file, e.g. thumbnails """ try: - if os.path.exists(self.get_thumbnail_folder()): - shutil.rmtree(self.get_thumbnail_folder()) - if os.path.exists(self.get_temp_folder()): - shutil.rmtree(self.get_temp_folder()) + thumbnail_folder_path = self.get_thumbnail_folder() + temp_folder_path = self.get_temp_folder() + if thumbnail_folder_path.exists(): + rmtree(thumbnail_folder_path) + if temp_folder_path.exists(): + rmtree(temp_folder_path) except OSError: log.exception('Failed to delete presentation controller files') @@ -132,24 +135,30 @@ class PresentationDocument(object): def get_thumbnail_folder(self): """ The location where thumbnail images will be stored + + :return: The path to the thumbnail + :rtype: openlp.core.common.path.Path """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(self.file_path.encode('utf-8')) else: folder = self.get_file_name() - return os.path.join(self.controller.thumbnail_folder, folder) + return Path(self.controller.thumbnail_folder, folder) def get_temp_folder(self): """ The location where thumbnail images will be stored + + :return: The path to the temporary file folder + :rtype: openlp.core.common.path.Path """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(self.file_path.encode('utf-8')) else: - folder = folder = self.get_file_name() - return os.path.join(self.controller.temp_folder, folder) + folder = self.get_file_name() + return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ @@ -251,15 +260,17 @@ class PresentationDocument(object): thumb_path = self.get_thumbnail_path(idx, False) create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) - def get_thumbnail_path(self, slide_no, check_exists): + def get_thumbnail_path(self, slide_no, check_exists=True): """ Returns an image path containing a preview for the requested slide - :param slide_no: The slide an image is required for, starting at 1 - :param check_exists: + :param int slide_no: The slide an image is required for, starting at 1 + :param bool check_exists: Check if the generated path exists + :return: The path, or None if the :param:`check_exists` is True and the file does not exist + :rtype: openlp.core.common.path.Path, None """ - path = os.path.join(self.get_thumbnail_folder(), self.controller.thumbnail_prefix + str(slide_no) + '.png') - if os.path.isfile(path) or not check_exists: + path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png') + if path.is_file() or not check_exists: return path else: return None @@ -304,7 +315,7 @@ class PresentationDocument(object): """ titles = [] notes = [] - titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + titles_file = str(self.get_thumbnail_folder() / 'titles.txt') if os.path.exists(titles_file): try: with open(titles_file, encoding='utf-8') as fi: @@ -313,7 +324,7 @@ class PresentationDocument(object): log.exception('Failed to open/read existing titles file') titles = [] for slide_no, title in enumerate(titles, 1): - notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes{number:d}.txt'.format(number=slide_no)) + notes_file = str(self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)) note = '' if os.path.exists(notes_file): try: @@ -331,14 +342,13 @@ class PresentationDocument(object): and notes to the slideNote%.txt """ if titles: - titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') - with open(titles_file, mode='wt', encoding='utf-8') as fo: + titles_path = self.get_thumbnail_folder() / 'titles.txt' + with titles_path.open(mode='wt', encoding='utf-8') as fo: fo.writelines(titles) if notes: for slide_no, note in enumerate(notes, 1): - notes_file = os.path.join(self.get_thumbnail_folder(), - 'slideNotes{number:d}.txt'.format(number=slide_no)) - with open(notes_file, mode='wt', encoding='utf-8') as fn: + notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) + with notes_path.open(mode='wt', encoding='utf-8') as fn: fn.write(note) diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index b50cd0a0c..066a0ea26 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -48,7 +48,7 @@ def report_song_list(): Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')), translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) - if not report_file_path: + if report_file_path is None: main_window.error_message( translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n' diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index 37a040aa6..40eb19ac8 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -103,7 +103,7 @@ class TestExceptionForm(TestMixin, TestCase): os.remove(self.tempfile) @patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog") - @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog") + @patch("openlp.core.ui.exceptionform.FileDialog") @patch("openlp.core.ui.exceptionform.QtCore.QUrl") @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem") @patch("openlp.core.ui.exceptionform.Qt") From a6324b6b7f3b6a0266eac81bd9e604fa6525be15 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 8 Sep 2017 22:19:22 -0700 Subject: [PATCH 05/52] Refactor version check threading --- openlp.py | 8 +- openlp/core/__init__.py | 40 ++--- openlp/core/lib/plugin.py | 4 +- openlp/core/threading.py | 55 ++++++ openlp/core/ui/aboutform.py | 4 +- openlp/core/ui/exceptionform.py | 4 +- openlp/core/ui/generaltab.py | 1 - openlp/core/ui/mainwindow.py | 17 +- .../{common/versionchecker.py => version.py} | 162 ++++++++---------- 9 files changed, 171 insertions(+), 124 deletions(-) create mode 100644 openlp/core/threading.py rename openlp/core/{common/versionchecker.py => version.py} (57%) diff --git a/openlp.py b/openlp.py index 7ede25519..68001f2d1 100755 --- a/openlp.py +++ b/openlp.py @@ -20,13 +20,17 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import sys +""" +The entrypoint for OpenLP +""" +import faulthandler import multiprocessing +import sys from openlp.core.common import is_win, is_macosx from openlp.core import main +faulthandler.enable() if __name__ == '__main__': """ diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index af29cddae..d7c21026e 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -26,21 +26,19 @@ The :mod:`core` module provides all core application functions All the core functions of the OpenLP application including the GUI, settings, logging and a plugin framework are contained within the openlp.core module. """ - import argparse import logging import os import shutil import sys import time -from pathlib import Path from traceback import format_exception from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, is_macosx, is_win, translate -from openlp.core.common.versionchecker import VersionThread, get_application_version +from openlp.core.version import check_for_update, get_version from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen @@ -154,8 +152,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): self.processEvents() if not has_run_wizard: self.main_window.first_time() - version = VersionThread(self.main_window) - version.start() + if Settings().value('core/update check'): + check_for_update(self.main_window) self.main_window.is_display_blank() self.main_window.app_startup() return self.exec() @@ -183,22 +181,18 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): data_folder_path = str(AppLocation.get_data_path()) if not os.path.exists(data_folder_path): log.critical('Database was not found in: ' + data_folder_path) - status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'), - translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}' - '\n\nThe location of the data folder was ' - 'previously changed from the OpenLP\'s ' - 'default location. If the data was stored on ' - 'removable device, that device needs to be ' - 'made available.\n\nYou may reset the data ' - 'location back to the default location, ' - 'or you can try to make the current location ' - 'available.\n\nDo you want to reset to the ' - 'default data location? If not, OpenLP will be ' - 'closed so you can try to fix the the problem.') - .format(path=data_folder_path), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No), - QtWidgets.QMessageBox.No) + status = QtWidgets.QMessageBox.critical( + None, translate('OpenLP', 'Data Directory Error'), + translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data ' + 'folder was previously changed from the OpenLP\'s default location. If the data was ' + 'stored on removable device, that device needs to be made available.\n\nYou may reset ' + 'the data location back to the default location, or you can try to make the current ' + 'location available.\n\nDo you want to reset to the default data location? If not, ' + 'OpenLP will be closed so you can try to fix the the problem.').format( + path=data_folder_path), + QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), + QtWidgets.QMessageBox.No + ) if status == QtWidgets.QMessageBox.No: # If answer was "No", return "True", it will shutdown OpenLP in def main log.info('User requested termination') @@ -239,7 +233,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): :param can_show_splash: Should OpenLP show the splash screen """ data_version = Settings().value('core/application version') - openlp_version = get_application_version()['version'] + openlp_version = get_version()['version'] # New installation, no need to create backup if not has_run_wizard: Settings().setValue('core/application version', openlp_version) @@ -415,7 +409,7 @@ def main(args=None): Registry.create() Registry().register('application', application) Registry().set_flag('no_web_server', args.no_web_server) - application.setApplicationVersion(get_application_version()['version']) + application.setApplicationVersion(get_version()['version']) # Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one if application.is_already_running(): sys.exit() diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index b06e0fbd4..d06385864 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -27,7 +27,7 @@ import logging from PyQt5 import QtCore from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings -from openlp.core.common.versionchecker import get_application_version +from openlp.core.version import get_version log = logging.getLogger(__name__) @@ -139,7 +139,7 @@ class Plugin(QtCore.QObject, RegistryProperties): if version: self.version = version else: - self.version = get_application_version()['version'] + self.version = get_version()['version'] self.settings_section = self.name self.icon = None self.media_item_class = media_item_class diff --git a/openlp/core/threading.py b/openlp/core/threading.py new file mode 100644 index 000000000..3eda2e436 --- /dev/null +++ b/openlp/core/threading.py @@ -0,0 +1,55 @@ +# -*- 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 # +############################################################################### +""" +The :mod:`openlp.core.threading` module contains some common threading code +""" +from PyQt5 import QtCore + + +def run_thread(parent, worker, prefix='', auto_start=True): + """ + Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase. + + :param object parent: The parent object so that the thread and worker are not orphaned. + :param QObject worker: A QObject-based worker object which does the actual work. + :param str prefix: A prefix to be applied to the attribute names. + :param bool auto_start: Automatically start the thread. Defaults to True. + """ + # Set up attribute names + thread_name = 'thread' + worker_name = 'worker' + if prefix: + thread_name = '_'.join([prefix, thread_name]) + worker_name = '_'.join([prefix, worker_name]) + # Create the thread and add the thread and the worker to the parent + thread = QtCore.QThread() + setattr(parent, thread_name, thread) + setattr(parent, worker_name, worker) + # Move the worker into the thread's context + worker.moveToThread(thread) + # Connect slots and signals + parent.version_thread.started.connect(parent.version_worker.start) + parent.version_worker.quit.connect(parent.version_thread.quit) + parent.version_worker.quit.connect(parent.version_worker.deleteLater) + parent.version_thread.finished.connect(parent.version_thread.deleteLater) + if auto_start: + parent.version_thread.start() diff --git a/openlp/core/ui/aboutform.py b/openlp/core/ui/aboutform.py index e1768b127..bed83785b 100644 --- a/openlp/core/ui/aboutform.py +++ b/openlp/core/ui/aboutform.py @@ -26,7 +26,7 @@ import webbrowser from PyQt5 import QtCore, QtWidgets -from openlp.core.common.versionchecker import get_application_version +from openlp.core.version import get_version from openlp.core.lib import translate from .aboutdialog import UiAboutDialog @@ -49,7 +49,7 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog): Set up the dialog. This method is mocked out in tests. """ self.setup_ui(self) - application_version = get_application_version() + application_version = get_version() about_text = self.about_text_edit.toPlainText() about_text = about_text.replace('', application_version['version']) if application_version['build']: diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 349352d92..c8a1753c2 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -71,7 +71,7 @@ except ImportError: VLC_VERSION = '-' from openlp.core.common import Settings, UiStrings, translate -from openlp.core.common.versionchecker import get_application_version +from openlp.core.version import get_version from openlp.core.common import RegistryProperties, is_linux from .exceptiondialog import Ui_ExceptionDialog @@ -110,7 +110,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): """ Create an exception report. """ - openlp_version = get_application_version() + openlp_version = get_version() description = self.description_text_edit.toPlainText() traceback = self.exception_text_edit.toPlainText() system = translate('OpenLP.ExceptionForm', 'Platform: {platform}\n').format(platform=platform.platform()) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index dc084eb2b..d3c44ff6c 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -164,7 +164,6 @@ class GeneralTab(SettingsTab): self.startup_layout.addWidget(self.show_splash_check_box) self.check_for_updates_check_box = QtWidgets.QCheckBox(self.startup_group_box) self.check_for_updates_check_box.setObjectName('check_for_updates_check_box') - self.check_for_updates_check_box.setVisible(False) self.startup_layout.addWidget(self.check_for_updates_check_box) self.right_layout.addWidget(self.startup_group_box) # Logo diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 54f70ccb2..a8f7d91c7 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -40,7 +40,6 @@ from openlp.core.api.http import server from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder -from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ @@ -51,6 +50,7 @@ from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.lib.dockwidget import OpenLPDockWidget from openlp.core.ui.lib.mediadockmanager import MediaDockManager +from openlp.core.version import get_version log = logging.getLogger(__name__) @@ -487,7 +487,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): """ The main window. """ - openlp_version_check = QtCore.pyqtSignal(QtCore.QVariant) log.info('MainWindow loaded') def __init__(self): @@ -561,7 +560,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.application.set_busy_cursor() # Simple message boxes Registry().register_function('theme_update_global', self.default_theme_changed) - self.openlp_version_check.connect(self.version_notice) Registry().register_function('config_screen_changed', self.screen_changed) Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) # Reset the cursor @@ -587,6 +585,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): if saved_plugin_id != -1: self.media_tool_box.setCurrentIndex(saved_plugin_id) + def on_new_version_number(self, version_number): + """ + Called when the version check thread completes and we need to check the version number + + :param str version_number: The version number downloaded from the OpenLP server. + """ + def on_search_shortcut_triggered(self): """ Called when the search shortcut has been pressed. @@ -606,7 +611,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): if widget: widget.on_focus() - def version_notice(self, version): + def on_new_version(self, version): """ Notifies the user that a newer version of OpenLP is available. Triggered by delay thread and cannot display popup. @@ -616,7 +621,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): log.debug('version_notice') version_text = translate('OpenLP.MainWindow', 'Version {new} of OpenLP is now available for download (you are ' 'currently running version {current}). \n\nYou can download the latest version from ' - 'http://openlp.org/.').format(new=version, current=get_application_version()[u'full']) + 'http://openlp.org/.').format(new=version, current=get_version()[u'full']) QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text) def show(self): @@ -973,7 +978,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): # Add a header section. # This is to insure it's our conf file for import. now = datetime.now() - application_version = get_application_version() + application_version = get_version() # Write INI format using Qsettings. # Write our header. export_settings.beginGroup(self.header_section) diff --git a/openlp/core/common/versionchecker.py b/openlp/core/version.py similarity index 57% rename from openlp/core/common/versionchecker.py rename to openlp/core/version.py index 6129ee2aa..3fa6c006c 100644 --- a/openlp/core/common/versionchecker.py +++ b/openlp/core/version.py @@ -20,24 +20,21 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`openlp.core.common` module downloads the version details for OpenLP. +The :mod:`openlp.core.version` module downloads the version details for OpenLP. """ import logging import os import platform import sys -import time -import urllib.error -import urllib.parse -import urllib.request from datetime import datetime from distutils.version import LooseVersion from subprocess import Popen, PIPE +import requests from PyQt5 import QtCore -from openlp.core.common import AppLocation, Registry, Settings -from openlp.core.common.httputils import ping +from openlp.core.common import AppLocation, Settings +from openlp.core.threading import run_thread log = logging.getLogger(__name__) @@ -46,42 +43,87 @@ CONNECTION_TIMEOUT = 30 CONNECTION_RETRIES = 2 -class VersionThread(QtCore.QThread): +class VersionWorker(QtCore.QObject): """ - A special Qt thread class to fetch the version of OpenLP from the website. - This is threaded so that it doesn't affect the loading time of OpenLP. + A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it + doesn't affect the loading time of OpenLP. """ - def __init__(self, main_window): - """ - Constructor for the thread class. + new_version = QtCore.pyqtSignal(dict) + no_internet = QtCore.pyqtSignal() + quit = QtCore.pyqtSignal() - :param main_window: The main window Object. + def __init__(self, last_check_date): """ - log.debug("VersionThread - Initialise") - super(VersionThread, self).__init__(None) - self.main_window = main_window + Constructor for the version check worker. - def run(self): + :param string last_check_date: The last day we checked for a new version of OpenLP """ - Run the thread. + log.debug('VersionWorker - Initialise') + super(VersionWorker, self).__init__(None) + self.last_check_date = last_check_date + + def start(self): """ - self.sleep(1) - log.debug('Version thread - run') - found = ping("openlp.io") - Registry().set_flag('internet_present', found) - update_check = Settings().value('core/update check') - if found: - Registry().execute('get_website_version') - if update_check: - app_version = get_application_version() - version = check_latest_version(app_version) - log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)), - version2=LooseVersion(str(app_version['full'])))) - if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])): - self.main_window.openlp_version_check.emit('{version}'.format(version=version)) + Check the latest version of OpenLP against the version file on the OpenLP site. + + **Rules around versions and version files:** + + * If a version number has a build (i.e. -bzr1234), then it is a nightly. + * If a version number's minor version is an odd number, it is a development release. + * If a version number's minor version is an even number, it is a stable release. + """ + log.debug('VersionWorker - Start') + # I'm not entirely sure why this was here, I'm commenting it out until I hit the same scenario + # time.sleep(1) + current_version = get_version() + download_url = 'http://www.openlp.org/files/version.txt' + if current_version['build']: + download_url = 'http://www.openlp.org/files/nightly_version.txt' + elif int(current_version['version'].split('.')[1]) % 2 != 0: + download_url = 'http://www.openlp.org/files/dev_version.txt' + headers = { + 'User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'], + system=platform.system(), + release=platform.release()) + } + remote_version = None + retries = 0 + while retries < 3: + try: + response = requests.get(download_url, headers=headers) + remote_version = response.text + log.debug('New version found: %s', remote_version) + break + except requests.exceptions.ConnectionError: + log.exception('Unable to connect to OpenLP server to download version file') + self.no_internet.emit() + retries += 1 + except requests.exceptions.RequestException: + log.exception('Error occurred while connecting to OpenLP server to download version file') + retries += 1 + if remote_version and LooseVersion(remote_version) > LooseVersion(current_version['full']): + self.new_version.emit(remote_version) + self.quit.emit() -def get_application_version(): +def check_for_update(parent): + """ + Run a thread to download and check the version of OpenLP + + :param MainWindow parent: The parent object for the thread. Usually the OpenLP main window. + """ + last_check_date = Settings().value('core/last version test') + if datetime.date().strftime('%Y-%m-%d') <= last_check_date: + log.debug('Version check skipped, last checked today') + return + worker = VersionWorker(last_check_date) + worker.new_version.connect(parent.on_new_version) + # TODO: Use this to figure out if there's an Internet connection? + # worker.no_internet.connect(parent.on_no_internet) + run_thread(parent, worker, 'version') + + +def get_version(): """ Returns the application version of the running instance of OpenLP:: @@ -150,55 +192,3 @@ def get_application_version(): else: log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version'])) return APPLICATION_VERSION - - -def check_latest_version(current_version): - """ - Check the latest version of OpenLP against the version file on the OpenLP - site. - - **Rules around versions and version files:** - - * If a version number has a build (i.e. -bzr1234), then it is a nightly. - * If a version number's minor version is an odd number, it is a development release. - * If a version number's minor version is an even number, it is a stable release. - - :param current_version: The current version of OpenLP. - """ - version_string = current_version['full'] - # set to prod in the distribution config file. - settings = Settings() - settings.beginGroup('core') - last_test = settings.value('last version test') - this_test = str(datetime.now().date()) - settings.setValue('last version test', this_test) - settings.endGroup() - if last_test != this_test: - if current_version['build']: - req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt') - else: - version_parts = current_version['version'].split('.') - if int(version_parts[1]) % 2 != 0: - req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt') - else: - req = urllib.request.Request('http://www.openlp.org/files/version.txt') - req.add_header('User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'], - system=platform.system(), - release=platform.release())) - remote_version = None - retries = 0 - while True: - try: - remote_version = str(urllib.request.urlopen(req, None, - timeout=CONNECTION_TIMEOUT).read().decode()).strip() - except (urllib.error.URLError, ConnectionError): - if retries > CONNECTION_RETRIES: - log.exception('Failed to download the latest OpenLP version file') - else: - retries += 1 - time.sleep(0.1) - continue - break - if remote_version: - version_string = remote_version - return version_string From 47f96a32860eaa328f756ced6df2da580a5f30e8 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 8 Sep 2017 22:57:06 -0700 Subject: [PATCH 06/52] Duh. Can't have the old names in there, now can we? --- openlp/core/threading.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/core/threading.py b/openlp/core/threading.py index 3eda2e436..36dfd481f 100644 --- a/openlp/core/threading.py +++ b/openlp/core/threading.py @@ -47,9 +47,9 @@ def run_thread(parent, worker, prefix='', auto_start=True): # Move the worker into the thread's context worker.moveToThread(thread) # Connect slots and signals - parent.version_thread.started.connect(parent.version_worker.start) - parent.version_worker.quit.connect(parent.version_thread.quit) - parent.version_worker.quit.connect(parent.version_worker.deleteLater) - parent.version_thread.finished.connect(parent.version_thread.deleteLater) + thread.started.connect(worker.start) + worker.quit.connect(thread.quit) + worker.quit.connect(worker.deleteLater) + thread.finished.connect(thread.deleteLater) if auto_start: - parent.version_thread.start() + thread.start() From d1a47036cf644828ef2cfa9ffddeca92dd877a32 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 11 Sep 2017 22:52:38 -0700 Subject: [PATCH 07/52] Remove unnecessary slot --- openlp/core/ui/mainwindow.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 4cee4a1f4..16328d63a 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -586,13 +586,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): if saved_plugin_id != -1: self.media_tool_box.setCurrentIndex(saved_plugin_id) - def on_new_version_number(self, version_number): - """ - Called when the version check thread completes and we need to check the version number - - :param str version_number: The version number downloaded from the OpenLP server. - """ - def on_search_shortcut_triggered(self): """ Called when the search shortcut has been pressed. From 2f8cdc81e03869ba72b5983e5848d9906cac8c56 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 12 Sep 2017 23:08:38 -0700 Subject: [PATCH 08/52] Fix up the version tests and remove old references to 'versionchecker' --- openlp/core/ui/mainwindow.py | 13 ++ openlp/core/version.py | 38 ++-- openlp/plugins/songs/lib/openlyricsxml.py | 4 +- scripts/check_dependencies.py | 7 +- tests/functional/openlp_core/test_version.py | 204 ++++++++++++++++++ .../openlp_core_common/test_versionchecker.py | 64 ------ .../openlp_core_ui/test_aboutform.py | 8 +- .../openlp_core_ui/test_exceptionform.py | 63 ++---- tests/functional/test_init.py | 12 +- 9 files changed, 273 insertions(+), 140 deletions(-) create mode 100644 tests/functional/openlp_core/test_version.py delete mode 100644 tests/functional/openlp_core_common/test_versionchecker.py diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 16328d63a..ae32c0347 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -496,6 +496,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): """ super(MainWindow, self).__init__() Registry().register('main_window', self) + self.version_thread = None + self.version_worker = None self.clipboard = self.application.clipboard() self.arguments = ''.join(self.application.args) # Set up settings sections for the main application (not for use by plugins). @@ -1009,6 +1011,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): if not self.application.is_event_loop_active: event.ignore() return + # Sometimes the version thread hasn't finished, let's wait for it + try: + if self.version_thread and self.version_thread.isRunning(): + wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self) + wait_dialog.setWindowModality(QtCore.Qt.WindowModal) + wait_dialog.setAutoClose(False) + self.version_thread.wait() + wait_dialog.close() + except RuntimeError: + # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object + pass # If we just did a settings import, close without saving changes. if self.settings_imported: self.clean_up(False) diff --git a/openlp/core/version.py b/openlp/core/version.py index 3fa6c006c..9a532fcd8 100644 --- a/openlp/core/version.py +++ b/openlp/core/version.py @@ -26,7 +26,8 @@ import logging import os import platform import sys -from datetime import datetime +import time +from datetime import date from distutils.version import LooseVersion from subprocess import Popen, PIPE @@ -52,7 +53,7 @@ class VersionWorker(QtCore.QObject): no_internet = QtCore.pyqtSignal() quit = QtCore.pyqtSignal() - def __init__(self, last_check_date): + def __init__(self, last_check_date, current_version): """ Constructor for the version check worker. @@ -61,6 +62,7 @@ class VersionWorker(QtCore.QObject): log.debug('VersionWorker - Initialise') super(VersionWorker, self).__init__(None) self.last_check_date = last_check_date + self.current_version = current_version def start(self): """ @@ -74,15 +76,14 @@ class VersionWorker(QtCore.QObject): """ log.debug('VersionWorker - Start') # I'm not entirely sure why this was here, I'm commenting it out until I hit the same scenario - # time.sleep(1) - current_version = get_version() + time.sleep(1) download_url = 'http://www.openlp.org/files/version.txt' - if current_version['build']: + if self.current_version['build']: download_url = 'http://www.openlp.org/files/nightly_version.txt' - elif int(current_version['version'].split('.')[1]) % 2 != 0: + elif int(self.current_version['version'].split('.')[1]) % 2 != 0: download_url = 'http://www.openlp.org/files/dev_version.txt' headers = { - 'User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'], + 'User-Agent': 'OpenLP/{version} {system}/{release}; '.format(version=self.current_version['full'], system=platform.system(), release=platform.release()) } @@ -94,18 +95,23 @@ class VersionWorker(QtCore.QObject): remote_version = response.text log.debug('New version found: %s', remote_version) break - except requests.exceptions.ConnectionError: + except IOError: log.exception('Unable to connect to OpenLP server to download version file') - self.no_internet.emit() retries += 1 - except requests.exceptions.RequestException: - log.exception('Error occurred while connecting to OpenLP server to download version file') - retries += 1 - if remote_version and LooseVersion(remote_version) > LooseVersion(current_version['full']): + else: + self.no_internet.emit() + if remote_version and LooseVersion(remote_version) > LooseVersion(self.current_version['full']): self.new_version.emit(remote_version) self.quit.emit() +def update_check_date(): + """ + Save when we last checked for an update + """ + Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d')) + + def check_for_update(parent): """ Run a thread to download and check the version of OpenLP @@ -113,11 +119,12 @@ def check_for_update(parent): :param MainWindow parent: The parent object for the thread. Usually the OpenLP main window. """ last_check_date = Settings().value('core/last version test') - if datetime.date().strftime('%Y-%m-%d') <= last_check_date: + if date.today().strftime('%Y-%m-%d') <= last_check_date: log.debug('Version check skipped, last checked today') return - worker = VersionWorker(last_check_date) + worker = VersionWorker(last_check_date, get_version()) worker.new_version.connect(parent.on_new_version) + worker.quit.connect(update_check_date) # TODO: Use this to figure out if there's an Internet connection? # worker.no_internet.connect(parent.on_no_internet) run_thread(parent, worker, 'version') @@ -132,6 +139,7 @@ def get_version(): global APPLICATION_VERSION if APPLICATION_VERSION: return APPLICATION_VERSION + print(sys.argv) if '--dev-version' in sys.argv or '-d' in sys.argv: # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied # there. diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index 807ea5593..4819e61de 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -62,7 +62,7 @@ import re from lxml import etree, objectify from openlp.core.common import translate, Settings -from openlp.core.common.versionchecker import get_application_version +from openlp.core.version import get_version from openlp.core.lib import FormattingTags from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic @@ -234,7 +234,7 @@ class OpenLyrics(object): # Append the necessary meta data to the song. song_xml.set('xmlns', NAMESPACE) song_xml.set('version', OpenLyrics.IMPLEMENTED_VERSION) - application_name = 'OpenLP ' + get_application_version()['version'] + application_name = 'OpenLP ' + get_version()['version'] song_xml.set('createdIn', application_name) song_xml.set('modifiedIn', application_name) # "Convert" 2012-08-27 11:49:15 to 2012-08-27T11:49:15. diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 253cb330c..1d8ffff3e 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -26,7 +26,7 @@ This script is used to check dependencies of OpenLP. It checks availability of required python modules and their version. To verify availability of Python modules, simply run this script:: - @:~$ ./check_dependencies.py + $ ./check_dependencies.py """ import os @@ -45,7 +45,7 @@ IS_MAC = sys.platform.startswith('dar') VERS = { - 'Python': '3.0', + 'Python': '3.4', 'PyQt5': '5.0', 'Qt5': '5.0', 'sqlalchemy': '0.5', @@ -97,7 +97,8 @@ MODULES = [ 'asyncio', 'waitress', 'six', - 'webob' + 'webob', + 'requests' ] diff --git a/tests/functional/openlp_core/test_version.py b/tests/functional/openlp_core/test_version.py new file mode 100644 index 000000000..c5790a9b6 --- /dev/null +++ b/tests/functional/openlp_core/test_version.py @@ -0,0 +1,204 @@ +# -*- 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 # +############################################################################### +""" +Package to test the openlp.core.version package. +""" +import sys +from datetime import date +from unittest.mock import MagicMock, patch + +from requests.exceptions import ConnectionError + +from openlp.core.version import VersionWorker, check_for_update, get_version, update_check_date + + +def test_worker_init(): + """Test the VersionWorker constructor""" + # GIVEN: A last check date and a current version + last_check_date = '1970-01-01' + current_version = '2.0' + + # WHEN: A worker is created + worker = VersionWorker(last_check_date, current_version) + + # THEN: The correct attributes should have been set + assert worker.last_check_date == last_check_date + assert worker.current_version == current_version + + +@patch('openlp.core.version.platform') +@patch('openlp.core.version.requests') +def test_worker_start(mock_requests, mock_platform): + """Test the VersionWorkder.start() method""" + # GIVEN: A last check date, current version, and an instance of worker + last_check_date = '1970-01-01' + current_version = {'full': '2.0', 'version': '2.0', 'build': None} + mock_platform.system.return_value = 'Linux' + mock_platform.release.return_value = '4.12.0-1-amd64' + mock_requests.get.return_value = MagicMock(text='2.4.6') + worker = VersionWorker(last_check_date, current_version) + + # WHEN: The worker is run + with patch.object(worker, 'new_version') as mock_new_version, \ + patch.object(worker, 'quit') as mock_quit: + worker.start() + + # THEN: The check completes and the signal is emitted + expected_download_url = 'http://www.openlp.org/files/version.txt' + expected_headers = {'User-Agent': 'OpenLP/2.0 Linux/4.12.0-1-amd64; '} + mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers) + mock_new_version.emit.assert_called_once_with('2.4.6') + mock_quit.emit.assert_called_once_with() + + +@patch('openlp.core.version.platform') +@patch('openlp.core.version.requests') +def test_worker_start_dev_version(mock_requests, mock_platform): + """Test the VersionWorkder.start() method for dev versions""" + # GIVEN: A last check date, current version, and an instance of worker + last_check_date = '1970-01-01' + current_version = {'full': '2.1.3', 'version': '2.1.3', 'build': None} + mock_platform.system.return_value = 'Linux' + mock_platform.release.return_value = '4.12.0-1-amd64' + mock_requests.get.return_value = MagicMock(text='2.4.6') + worker = VersionWorker(last_check_date, current_version) + + # WHEN: The worker is run + with patch.object(worker, 'new_version') as mock_new_version, \ + patch.object(worker, 'quit') as mock_quit: + worker.start() + + # THEN: The check completes and the signal is emitted + expected_download_url = 'http://www.openlp.org/files/dev_version.txt' + expected_headers = {'User-Agent': 'OpenLP/2.1.3 Linux/4.12.0-1-amd64; '} + mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers) + mock_new_version.emit.assert_called_once_with('2.4.6') + mock_quit.emit.assert_called_once_with() + + +@patch('openlp.core.version.platform') +@patch('openlp.core.version.requests') +def test_worker_start_nightly_version(mock_requests, mock_platform): + """Test the VersionWorkder.start() method for nightlies""" + # GIVEN: A last check date, current version, and an instance of worker + last_check_date = '1970-01-01' + current_version = {'full': '2.1-bzr2345', 'version': '2.1', 'build': '2345'} + mock_platform.system.return_value = 'Linux' + mock_platform.release.return_value = '4.12.0-1-amd64' + mock_requests.get.return_value = MagicMock(text='2.4.6') + worker = VersionWorker(last_check_date, current_version) + + # WHEN: The worker is run + with patch.object(worker, 'new_version') as mock_new_version, \ + patch.object(worker, 'quit') as mock_quit: + worker.start() + + # THEN: The check completes and the signal is emitted + expected_download_url = 'http://www.openlp.org/files/nightly_version.txt' + expected_headers = {'User-Agent': 'OpenLP/2.1-bzr2345 Linux/4.12.0-1-amd64; '} + mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers) + mock_new_version.emit.assert_called_once_with('2.4.6') + mock_quit.emit.assert_called_once_with() + + +@patch('openlp.core.version.platform') +@patch('openlp.core.version.requests') +def test_worker_start_connection_error(mock_requests, mock_platform): + """Test the VersionWorkder.start() method when a ConnectionError happens""" + # GIVEN: A last check date, current version, and an instance of worker + last_check_date = '1970-01-01' + current_version = {'full': '2.0', 'version': '2.0', 'build': None} + mock_platform.system.return_value = 'Linux' + mock_platform.release.return_value = '4.12.0-1-amd64' + mock_requests.get.side_effect = ConnectionError('Could not connect') + worker = VersionWorker(last_check_date, current_version) + + # WHEN: The worker is run + with patch.object(worker, 'no_internet') as mocked_no_internet, \ + patch.object(worker, 'quit') as mocked_quit: + worker.start() + + # THEN: The check completes and the signal is emitted + expected_download_url = 'http://www.openlp.org/files/version.txt' + expected_headers = {'User-Agent': 'OpenLP/2.0 Linux/4.12.0-1-amd64; '} + mock_requests.get.assert_called_with(expected_download_url, headers=expected_headers) + assert mock_requests.get.call_count == 3 + mocked_no_internet.emit.assert_called_once_with() + mocked_quit.emit.assert_called_once_with() + + +@patch('openlp.core.version.Settings') +def test_update_check_date(MockSettings): + """Test that the update_check_date() function writes the correct date""" + # GIVEN: A mocked Settings object + mocked_settings = MagicMock() + MockSettings.return_value = mocked_settings + + # WHEN: update_check_date() is called + update_check_date() + + # THEN: The correct date should have been saved + mocked_settings.setValue.assert_called_once_with('core/last version test', date.today().strftime('%Y-%m-%d')) + + +@patch('openlp.core.version.Settings') +@patch('openlp.core.version.run_thread') +def test_check_for_update(mocked_run_thread, MockSettings): + """Test the check_for_update() function""" + # GIVEN: A mocked settings object + mocked_settings = MagicMock() + mocked_settings.value.return_value = '1970-01-01' + MockSettings.return_value = mocked_settings + + # WHEN: check_for_update() is called + check_for_update(MagicMock()) + + # THEN: The right things should have been called and a thread set in motion + assert mocked_run_thread.call_count == 1 + + +@patch('openlp.core.version.Settings') +@patch('openlp.core.version.run_thread') +def test_check_for_update_skipped(mocked_run_thread, MockSettings): + """Test that the check_for_update() function skips running if it already ran today""" + # GIVEN: A mocked settings object + mocked_settings = MagicMock() + mocked_settings.value.return_value = date.today().strftime('%Y-%m-%d') + MockSettings.return_value = mocked_settings + + # WHEN: check_for_update() is called + check_for_update(MagicMock()) + + # THEN: The right things should have been called and a thread set in motion + assert mocked_run_thread.call_count == 0 + + +def test_get_version_dev_version(): + """Test the get_version() function""" + # GIVEN: We're in dev mode + with patch.object(sys, 'argv', ['--dev-version']), \ + patch('openlp.core.version.APPLICATION_VERSION', None): + # WHEN: get_version() is run + version = get_version() + + # THEN: version is something + assert version diff --git a/tests/functional/openlp_core_common/test_versionchecker.py b/tests/functional/openlp_core_common/test_versionchecker.py deleted file mode 100644 index 022a5f06f..000000000 --- a/tests/functional/openlp_core_common/test_versionchecker.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- 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 # -############################################################################### -""" -Package to test the openlp.core.common.versionchecker package. -""" -from unittest import TestCase -from unittest.mock import MagicMock, patch - -from openlp.core.common.settings import Settings -from openlp.core.common.versionchecker import VersionThread - -from tests.helpers.testmixin import TestMixin - - -class TestVersionchecker(TestMixin, TestCase): - - def setUp(self): - """ - Create an instance and a few example actions. - """ - self.build_settings() - - def tearDown(self): - """ - Clean up - """ - self.destroy_settings() - - def test_version_thread_triggered(self): - """ - Test the version thread call does not trigger UI - :return: - """ - # GIVEN: a equal version setup and the data is not today. - mocked_main_window = MagicMock() - Settings().setValue('core/last version test', '1950-04-01') - # WHEN: We check to see if the version is different . - with patch('PyQt5.QtCore.QThread'),\ - patch('openlp.core.common.versionchecker.get_application_version') as mocked_get_application_version: - mocked_get_application_version.return_value = {'version': '1.0.0', 'build': '', 'full': '2.0.4'} - version_thread = VersionThread(mocked_main_window) - version_thread.run() - # THEN: If the version has changed the main window is notified - self.assertTrue(mocked_main_window.openlp_version_check.emit.called, - 'The main windows should have been notified') diff --git a/tests/functional/openlp_core_ui/test_aboutform.py b/tests/functional/openlp_core_ui/test_aboutform.py index 817c13c48..c30ef588e 100644 --- a/tests/functional/openlp_core_ui/test_aboutform.py +++ b/tests/functional/openlp_core_ui/test_aboutform.py @@ -47,13 +47,13 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: A web browser is opened mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute') - @patch('openlp.core.ui.aboutform.get_application_version') - def test_about_form_build_number(self, mocked_get_application_version): + @patch('openlp.core.ui.aboutform.get_version') + def test_about_form_build_number(self, mocked_get_version): """ Test that the build number is added to the about form """ - # GIVEN: A mocked out get_application_version function - mocked_get_application_version.return_value = {'version': '3.1.5', 'build': '3000'} + # GIVEN: A mocked out get_version function + mocked_get_version.return_value = {'version': '3.1.5', 'build': '3000'} # WHEN: The about form is created about_form = AboutForm(None) diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index 37a040aa6..4bc83dbcd 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -53,7 +53,7 @@ MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of @patch("openlp.core.ui.exceptionform.Qt.qVersion") @patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl") -@patch("openlp.core.ui.exceptionform.get_application_version") +@patch("openlp.core.ui.exceptionform.get_version") @patch("openlp.core.ui.exceptionform.sqlalchemy") @patch("openlp.core.ui.exceptionform.bs4") @patch("openlp.core.ui.exceptionform.etree") @@ -64,18 +64,10 @@ class TestExceptionForm(TestMixin, TestCase): """ Test functionality of exception form functions """ - def __method_template_for_class_patches(self, - __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__, - mocked_python_version, - mocked_platform, - mocked_is_linux, - mocked_etree, - mocked_bs4, - mocked_sqlalchemy, - mocked_application_version, - mocked_openlurl, - mocked_qversion, - ): + def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__, + mocked_python_version, mocked_platform, mocked_is_linux, + mocked_etree, mocked_bs4, mocked_sqlalchemy, mocked_get_version, + mocked_openlurl, mocked_qversion): """ Template so you don't have to remember the layout of class mock options for methods """ @@ -86,7 +78,7 @@ class TestExceptionForm(TestMixin, TestCase): mocked_platform.return_value = 'Nose Test' mocked_qversion.return_value = 'Qt5 test' mocked_is_linux.return_value = False - mocked_application_version.return_value = 'Trunk Test' + mocked_get_version.return_value = 'Trunk Test' def setUp(self): self.setup_application() @@ -107,22 +99,10 @@ class TestExceptionForm(TestMixin, TestCase): @patch("openlp.core.ui.exceptionform.QtCore.QUrl") @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem") @patch("openlp.core.ui.exceptionform.Qt") - def test_on_send_report_button_clicked(self, - mocked_qt, - mocked_add_query_item, - mocked_qurl, - mocked_file_dialog, - mocked_ui_exception_dialog, - mocked_python_version, - mocked_platform, - mocked_is_linux, - mocked_etree, - mocked_bs4, - mocked_sqlalchemy, - mocked_application_version, - mocked_openlurl, - mocked_qversion, - ): + def test_on_send_report_button_clicked(self, mocked_qt, mocked_add_query_item, mocked_qurl, mocked_file_dialog, + mocked_ui_exception_dialog, mocked_python_version, mocked_platform, + mocked_is_linux, mocked_etree, mocked_bs4, mocked_sqlalchemy, + mocked_get_version, mocked_openlurl, mocked_qversion): """ Test send report creates the proper system information text """ @@ -134,10 +114,10 @@ class TestExceptionForm(TestMixin, TestCase): mocked_platform.return_value = 'Nose Test' mocked_qversion.return_value = 'Qt5 test' mocked_is_linux.return_value = False - mocked_application_version.return_value = 'Trunk Test' + mocked_get_version.return_value = 'Trunk Test' mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_is_linux.return_value = False - mocked_application_version.return_value = 'Trunk Test' + mocked_get_version.return_value = 'Trunk Test' test_form = exceptionform.ExceptionForm() test_form.file_attachment = None @@ -157,19 +137,10 @@ class TestExceptionForm(TestMixin, TestCase): @patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName") @patch("openlp.core.ui.exceptionform.Qt") - def test_on_save_report_button_clicked(self, - mocked_qt, - mocked_save_filename, - mocked_python_version, - mocked_platform, - mocked_is_linux, - mocked_etree, - mocked_bs4, - mocked_sqlalchemy, - mocked_application_version, - mocked_openlurl, - mocked_qversion, - ): + def test_on_save_report_button_clicked(self, mocked_qt, mocked_save_filename, mocked_python_version, + mocked_platform, mocked_is_linux, mocked_etree, mocked_bs4, + mocked_sqlalchemy, mocked_get_version, mocked_openlurl, + mocked_qversion): """ Test save report saves the correct information to a file """ @@ -181,7 +152,7 @@ class TestExceptionForm(TestMixin, TestCase): mocked_qversion.return_value = 'Qt5 test' mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_is_linux.return_value = False - mocked_application_version.return_value = 'Trunk Test' + mocked_get_version.return_value = 'Trunk Test' mocked_save_filename.return_value = (Path('testfile.txt'), 'filter') test_form = exceptionform.ExceptionForm() diff --git a/tests/functional/test_init.py b/tests/functional/test_init.py index 4cb6e5a76..3f30b253e 100644 --- a/tests/functional/test_init.py +++ b/tests/functional/test_init.py @@ -24,11 +24,11 @@ Package to test the openlp.core.__init__ package. """ import os from unittest import TestCase -from unittest.mock import MagicMock, patch, call +from unittest.mock import MagicMock, patch from PyQt5 import QtCore, QtWidgets -from openlp.core import OpenLP, parse_options +from openlp.core import OpenLP from openlp.core.common import Settings from tests.helpers.testmixin import TestMixin @@ -96,9 +96,9 @@ class TestInit(TestCase, TestMixin): 'build': 'bzr000' } Settings().setValue('core/application version', '2.2.0') - with patch('openlp.core.get_application_version') as mocked_get_application_version,\ + with patch('openlp.core.get_version') as mocked_get_version,\ patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: - mocked_get_application_version.return_value = MOCKED_VERSION + mocked_get_version.return_value = MOCKED_VERSION mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created @@ -122,9 +122,9 @@ class TestInit(TestCase, TestMixin): Settings().setValue('core/application version', '2.0.5') self.openlp.splash = MagicMock() self.openlp.splash.isVisible.return_value = True - with patch('openlp.core.get_application_version') as mocked_get_application_version,\ + with patch('openlp.core.get_version') as mocked_get_version, \ patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: - mocked_get_application_version.return_value = MOCKED_VERSION + mocked_get_version.return_value = MOCKED_VERSION mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created From 8ed5903ced06462290dac9f959230a1ade566138 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 15 Sep 2017 20:01:09 +0100 Subject: [PATCH 09/52] Moved most of the presentation plugin over to pathlib --- openlp/core/lib/__init__.py | 20 ++- openlp/core/lib/mediamanageritem.py | 2 - openlp/core/lib/shutil.py | 17 ++ .../presentations/lib/impresscontroller.py | 45 +++-- openlp/plugins/presentations/lib/mediaitem.py | 156 +++++++++--------- .../presentations/lib/messagelistener.py | 29 ++-- .../presentations/lib/pdfcontroller.py | 83 +++++----- .../presentations/lib/powerpointcontroller.py | 12 +- .../presentations/lib/pptviewcontroller.py | 35 ++-- .../lib/presentationcontroller.py | 119 ++++++------- .../presentations/lib/presentationtab.py | 9 +- .../test_presentationcontroller.py | 17 -- 12 files changed, 278 insertions(+), 266 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 08b675000..0babbc0d1 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -29,9 +29,10 @@ import os import re import math -from PyQt5 import QtCore, QtGui, Qt, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import translate +from openlp.core.common.path import Path log = logging.getLogger(__name__ + '.__init__') @@ -125,10 +126,11 @@ def build_icon(icon): Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name. - :param icon: - The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file - location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. + :param QtGui.QIcon | Path | QtGui.QIcon | str icon: + The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path + location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string. :return: The build icon. + :rtype: QtGui.QIcon """ if isinstance(icon, QtGui.QIcon): return icon @@ -136,6 +138,8 @@ def build_icon(icon): button_icon = QtGui.QIcon() if isinstance(icon, str): pix_map = QtGui.QPixmap(icon) + elif isinstance(icon, Path): + pix_map = QtGui.QPixmap(str(icon)) elif isinstance(icon, QtGui.QImage): pix_map = QtGui.QPixmap.fromImage(icon) if pix_map: @@ -221,10 +225,12 @@ def validate_thumb(file_path, thumb_path): :param thumb_path: The path to the thumb. :return: True, False if the image has changed since the thumb was created. """ - if not os.path.exists(thumb_path): + file_path = Path(file_path) + thumb_path = Path(thumb_path) + if not thumb_path.exists(): return False - image_date = os.stat(file_path).st_mtime - thumb_date = os.stat(thumb_path).st_mtime + image_date = file_path.stat().st_mtime + thumb_date = thumb_path.stat().st_mtime return image_date <= thumb_date diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 59a0e4e33..1c7a5b4ef 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -359,10 +359,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): :param files: The files to be loaded. :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ - names = [] full_list = [] for count in range(self.list_view.count()): - names.append(self.list_view.item(count).text()) full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) duplicates_found = False files_added = False diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py index d2f6ae34d..44dea590a 100755 --- a/openlp/core/lib/shutil.py +++ b/openlp/core/lib/shutil.py @@ -95,3 +95,20 @@ def rmtree(*args, **kwargs): args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) return shutil.rmtree(*args, **kwargs) +# TODO: Test and tidy +def which(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + file_name = shutil.which(*args, **kwargs) + if file_name: + return str_to_path(file_name) + return None diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 7699946a0..472e07801 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -32,11 +32,14 @@ # http://nxsy.org/comparing-documents-with-openoffice-and-python import logging -import os import time -from openlp.core.common import is_win, Registry, delete_file -from openlp.core.common.path import Path +from PyQt5 import QtCore + +from openlp.core.common import Registry, delete_file, get_uno_command, get_uno_instance, is_win +from openlp.core.lib import ScreenList +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ + TextType if is_win(): from win32com.client import Dispatch @@ -55,14 +58,6 @@ else: except ImportError: uno_available = False -from PyQt5 import QtCore - -from openlp.core.lib import ScreenList -from openlp.core.common import get_uno_command, get_uno_instance -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ - TextType - - log = logging.getLogger(__name__) @@ -203,12 +198,15 @@ class ImpressDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path for the document to load + :rtype: None """ log.debug('Init Presentation OpenOffice') - super(ImpressDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.document = None self.presentation = None self.control = None @@ -225,10 +223,10 @@ class ImpressDocument(PresentationDocument): if desktop is None: self.controller.start_process() desktop = self.controller.get_com_desktop() - url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20') + url = self.file_path.as_uri() else: desktop = self.controller.get_uno_desktop() - url = uno.systemPathToFileUrl(self.file_path) + url = uno.systemPathToFileUrl(str(self.file_path)) if desktop is None: return False self.desktop = desktop @@ -254,11 +252,11 @@ class ImpressDocument(PresentationDocument): log.debug('create thumbnails OpenOffice') if self.check_thumbnails(): return + temp_folder_path = self.get_temp_folder() if is_win(): - thumb_dir_url = 'file:///' + str(self.get_temp_folder()).replace('\\', '/') \ - .replace(':', '|').replace(' ', '%20') + thumb_dir_url = temp_folder_path.as_uri() else: - thumb_dir_url = uno.systemPathToFileUrl(str(self.get_temp_folder())) + thumb_dir_url = uno.systemPathToFileUrl(str(temp_folder_path)) properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) @@ -266,17 +264,16 @@ class ImpressDocument(PresentationDocument): pages = doc.getDrawPages() if not pages: return - temp_folder_path = self.get_temp_folder() - if not temp_folder_path.isdir(): - temp_folder_path.mkdir() + if not temp_folder_path.is_dir(): + temp_folder_path.mkdir(parents=True) for index in range(pages.getCount()): page = pages.getByIndex(index) doc.getCurrentController().setCurrentPage(page) - url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = temp_folder_path / '{number).png'.format(number=index + 1) + url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1) + path = temp_folder_path / '{number:d}.png'.format(number=index + 1) try: doc.storeToURL(url_path, properties) - self.convert_thumbnail(str(path), index + 1) + self.convert_thumbnail(path, index + 1) delete_file(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 275279e15..aa5bfc0d6 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -19,15 +19,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.common.languagemanager import get_locale_key -from openlp.core.common.path import path_to_str +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box @@ -128,7 +126,7 @@ class PresentationMediaItem(MediaManagerItem): """ self.list_view.setIconSize(QtCore.QSize(88, 50)) file_paths = Settings().value(self.settings_section + '/presentations files') - self.load_list([path_to_str(file) for file in file_paths], initial_load=True) + self.load_list([path_to_str(path) for path in file_paths], initial_load=True) self.populate_display_types() def populate_display_types(self): @@ -152,54 +150,57 @@ class PresentationMediaItem(MediaManagerItem): else: self.presentation_widget.hide() - def load_list(self, files, target_group=None, initial_load=False): + def load_list(self, file_paths, target_group=None, initial_load=False): """ Add presentations into the media manager. This is called both on initial load of the plugin to populate with existing files, and when the user adds new files via the media manager. + + :param list[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager. """ - current_list = self.get_file_list() - titles = [file_path.name for file_path in current_list] + file_paths = [str_to_path(filename) for filename in file_paths] + current_paths = self.get_file_list() + titles = [file_path.name for file_path in current_paths] self.application.set_busy_cursor() if not initial_load: - self.main_window.display_progress_bar(len(files)) + self.main_window.display_progress_bar(len(file_paths)) # Sort the presentations by its filename considering language specific characters. - files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) - for file in files: + file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) + for file_path in file_paths: if not initial_load: self.main_window.increment_progress_bar() - if current_list.count(file) > 0: + if current_paths.count(file_path) > 0: continue - filename = os.path.split(file)[1] - if not os.path.exists(file): - item_name = QtWidgets.QListWidgetItem(filename) + file_name = file_path.name + if not file_path.exists(): + item_name = QtWidgets.QListWidgetItem(file_name) item_name.setIcon(build_icon(ERROR_IMAGE)) - item_name.setData(QtCore.Qt.UserRole, file) - item_name.setToolTip(file) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) else: - if titles.count(filename) > 0: + if titles.count(file_name) > 0: if not initial_load: critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'), translate('PresentationPlugin.MediaItem', 'A presentation with that filename already exists.')) continue - controller_name = self.find_controller_by_type(filename) + controller_name = self.find_controller_by_type(file_path) if controller_name: controller = self.controllers[controller_name] - doc = controller.add_document(file) - thumb = str(doc.get_thumbnail_folder() / 'icon.png') - preview = doc.get_thumbnail_path(1, True) - if not preview and not initial_load: + doc = controller.add_document(file_path) + thumbnail_path = doc.get_thumbnail_folder() / 'icon.png' + preview_path = doc.get_thumbnail_path(1, True) + if not preview_path and not initial_load: doc.load_presentation() - preview = doc.get_thumbnail_path(1, True) + preview_path = doc.get_thumbnail_path(1, True) doc.close_presentation() - if not (preview and os.path.exists(preview)): + if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(preview, thumb): - icon = build_icon(thumb) + if validate_thumb(preview_path, thumbnail_path): + icon = build_icon(thumbnail_path) else: - icon = create_thumb(preview, thumb) + icon = create_thumb(str(preview_path), str(thumbnail_path)) else: if initial_load: icon = build_icon(':/general/general_delete.png') @@ -208,10 +209,10 @@ class PresentationMediaItem(MediaManagerItem): translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.')) continue - item_name = QtWidgets.QListWidgetItem(filename) - item_name.setData(QtCore.Qt.UserRole, file) + item_name = QtWidgets.QListWidgetItem(file_name) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) item_name.setIcon(icon) - item_name.setToolTip(file) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) if not initial_load: self.main_window.finished_progress_bar() @@ -228,8 +229,8 @@ class PresentationMediaItem(MediaManagerItem): self.application.set_busy_cursor() self.main_window.display_progress_bar(len(row_list)) for item in items: - filepath = str(item.data(QtCore.Qt.UserRole)) - self.clean_up_thumbnails(filepath) + file_path = str_to_path(item.data(QtCore.Qt.UserRole)) + self.clean_up_thumbnails(file_path) self.main_window.increment_progress_bar() self.main_window.finished_progress_bar() for row in row_list: @@ -237,30 +238,29 @@ class PresentationMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + '/presentations files', self.get_file_list()) self.application.set_normal_cursor() - def clean_up_thumbnails(self, filepath, clean_for_update=False): + def clean_up_thumbnails(self, file_path, clean_for_update=False): """ Clean up the files created such as thumbnails - :param filepath: File path of the presention to clean up after - :param clean_for_update: Only clean thumbnails if update is needed - :return: None + :param openlp.core.common.path.Path file_path: File path of the presention to clean up after + :param bool clean_for_update: Only clean thumbnails if update is needed + :rtype: None """ for cidx in self.controllers: - root, file_ext = os.path.splitext(filepath) - file_ext = file_ext[1:] + file_ext = file_path.suffix[1:] if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports: - doc = self.controllers[cidx].add_document(filepath) + doc = self.controllers[cidx].add_document(file_path) if clean_for_update: thumb_path = doc.get_thumbnail_path(1, True) - if not thumb_path or not os.path.exists(filepath) or os.path.getmtime( - thumb_path) < os.path.getmtime(filepath): + if not thumb_path or not file_path.exists() or \ + thumb_path.stat().st_mtime < file_path.stat().st_mtime: doc.presentation_deleted() else: doc.presentation_deleted() doc.close_presentation() def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, - context=ServiceItemContext.Service, presentation_file=None): + context=ServiceItemContext.Service, file_path=None): """ Generate the slide data. Needs to be implemented by the plugin. @@ -276,10 +276,9 @@ class PresentationMediaItem(MediaManagerItem): items = self.list_view.selectedItems() if len(items) > 1: return False - filename = presentation_file - if filename is None: - filename = items[0].data(QtCore.Qt.UserRole) - file_type = os.path.splitext(filename.lower())[1][1:] + if file_path is None: + file_path = str_to_path(items[0].data(QtCore.Qt.UserRole)) + file_type = file_path.suffix.lower()[1:] if not self.display_type_combo_box.currentText(): return False service_item.add_capability(ItemCapabilities.CanEditTitle) @@ -292,29 +291,28 @@ class PresentationMediaItem(MediaManagerItem): # force a nonexistent theme service_item.theme = -1 for bitem in items: - filename = presentation_file - if filename is None: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): - processor = self.find_controller_by_type(filename) + if file_path is None: + file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) + path, file_name = file_path.parent, file_path.name + service_item.title = file_name + if file_path.exists(): + processor = self.find_controller_by_type(file_path) if not processor: return False controller = self.controllers[processor] service_item.processor = None - doc = controller.add_document(filename) + doc = controller.add_document(file_path) if doc.get_thumbnail_path(1, True) is None or \ - not (doc.get_temp_folder() / 'mainslide001.png').is_file(): + not (doc.get_temp_folder() / 'mainslide001.png').is_file(): doc.load_presentation() i = 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) - while os.path.isfile(image): - service_item.add_from_image(image, name, thumbnail=thumbnail) + image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) + thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) + while image_path.is_file(): + service_item.add_from_image(str(image_path), file_name, thumbnail=str(thumbnail_path)) i += 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) + image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) + thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True @@ -324,34 +322,34 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).format(name=file_path)) return False else: service_item.processor = self.display_type_combo_box.currentText() service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) for bitem in items: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): + file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) + path, file_name = file_path.parent, file_path.name + service_item.title = file_name + if file_path.exists: if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic': - service_item.processor = self.find_controller_by_type(filename) + service_item.processor = self.find_controller_by_type(file_path) if not service_item.processor: return False controller = self.controllers[service_item.processor] - doc = controller.add_document(filename) + doc = controller.add_document(file_path) if doc.get_thumbnail_path(1, True) is None: doc.load_presentation() i = 1 - img = doc.get_thumbnail_path(i, True) - if img: + thumbnail_path = doc.get_thumbnail_path(i, True) + if thumbnail_path: # Get titles and notes titles, notes = doc.get_titles_and_notes() service_item.add_capability(ItemCapabilities.HasDisplayTitle) if notes.count('') != len(notes): service_item.add_capability(ItemCapabilities.HasNotes) service_item.add_capability(ItemCapabilities.HasThumbnails) - while img: + while thumbnail_path: # Use title and note if available title = '' if titles and len(titles) >= i: @@ -359,9 +357,9 @@ class PresentationMediaItem(MediaManagerItem): note = '' if notes and len(notes) >= i: note = notes[i - 1] - service_item.add_from_command(path, name, img, title, note) + service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note) i += 1 - img = doc.get_thumbnail_path(i, True) + thumbnail_path = doc.get_thumbnail_path(i, True) doc.close_presentation() return True else: @@ -371,7 +369,7 @@ class PresentationMediaItem(MediaManagerItem): 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} is incomplete, ' - 'please reload.').format(name=filename)) + 'please reload.').format(name=file_path)) return False else: # File is no longer present @@ -379,18 +377,20 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).format(name=file_path)) return False - def find_controller_by_type(self, filename): + def find_controller_by_type(self, file_path): """ Determine the default application controller to use for the selected file type. This is used if "Automatic" is set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension. If none found, then look for a controller which "also supports" it instead. - :param filename: The file name + :param openlp.core.common.path.Path file_path: The file path + :return: The default application controller for this file type, or None if not supported + :rtype: PresentationController """ - file_type = os.path.splitext(filename)[1][1:] + file_type = file_path.suffix[1:] if not file_type: return None for controller in self.controllers: diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 8e5de3e2d..5ad46d0fe 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -19,16 +19,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import logging import copy -import os +import logging from PyQt5 import QtCore from openlp.core.common import Registry, Settings -from openlp.core.ui import HideMode +from openlp.core.common.path import Path from openlp.core.lib import ServiceItemContext +from openlp.core.ui import HideMode from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES log = logging.getLogger(__name__) @@ -325,21 +324,25 @@ class MessageListener(object): is_live = message[1] item = message[0] hide_mode = message[2] - file = item.get_frame_path() + file_path = Path(item.get_frame_path()) self.handler = item.processor # When starting presentation from the servicemanager we convert # PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager # the conversion has already been done at this point. - file_type = os.path.splitext(file.lower())[1][1:] + file_type = file_path.suffix.lower()[1:] if file_type in PDF_CONTROLLER_FILETYPES: - log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file)) + log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path)) # Create a copy of the original item, and then clear the original item so it can be filled with images item_cpy = copy.copy(item) item.__init__(None) if is_live: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, + str(file_path)) else: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, + str(file_path)) # Some of the original serviceitem attributes is needed in the new serviceitem item.footer = item_cpy.footer item.from_service = item_cpy.from_service @@ -352,13 +355,13 @@ class MessageListener(object): self.handler = None else: if self.handler == self.media_item.automatic: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return else: - # the saved handler is not present so need to use one based on file suffix. + # the saved handler is not present so need to use one based on file_path suffix. if not self.controllers[self.handler].available: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return if is_live: @@ -370,7 +373,7 @@ class MessageListener(object): if self.handler is None: self.controller = controller else: - controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3]) + controller.add_handler(self.controllers[self.handler], file_path, hide_mode, message[3]) self.timer.start() def slide(self, message): diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index f80ad94ba..9f4aa1b4f 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -23,13 +23,13 @@ import os import logging import re -from shutil import which from subprocess import check_output, CalledProcessError from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import Settings, is_win from openlp.core.common.path import Path, path_to_str from openlp.core.lib import ScreenList +from openlp.core.lib.shutil import which from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument if is_win(): @@ -66,11 +66,12 @@ class PdfController(PresentationController): Function that checks whether a binary is either ghostscript or mudraw or neither. Is also used from presentationtab.py - :param program_path:The full path to the binary to check. + :param openlp.core.common.path.Path program_path: The full path to the binary to check. :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid. + :rtype: str | None """ program_type = None - runlog = check_binary_exists(Path(program_path)) + runlog = check_binary_exists(program_path) # Analyse the output to see it the program is mudraw, ghostscript or neither for line in runlog.splitlines(): decoded_line = line.decode() @@ -107,30 +108,29 @@ class PdfController(PresentationController): :return: True if program to open PDF-files was found, otherwise False. """ log.debug('check_installed Pdf') - self.mudrawbin = '' - self.mutoolbin = '' - self.gsbin = '' + self.mudrawbin = None + self.mutoolbin = None + self.gsbin = None self.also_supports = [] # Use the user defined program if given if Settings().value('presentations/enable_pdf_program'): - pdf_program = path_to_str(Settings().value('presentations/pdf_program')) - program_type = self.process_check_binary(pdf_program) + program_path = Settings().value('presentations/pdf_program') + program_type = self.process_check_binary(program_path) if program_type == 'gs': - self.gsbin = pdf_program + self.gsbin = program_path elif program_type == 'mudraw': - self.mudrawbin = pdf_program + self.mudrawbin = program_path elif program_type == 'mutool': - self.mutoolbin = pdf_program + self.mutoolbin = program_path else: # Fallback to autodetection - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) + application_path = AppLocation.get_directory(AppLocation.AppDir) if is_win(): # for windows we only accept mudraw.exe or mutool.exe in the base folder - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): - self.mudrawbin = os.path.join(application_path, 'mudraw.exe') - elif os.path.isfile(os.path.join(application_path, 'mutool.exe')): - self.mutoolbin = os.path.join(application_path, 'mutool.exe') + if (application_path / 'mudraw.exe').is_file(): + self.mudrawbin = application_path / 'mudraw.exe' + elif (application_path / 'mutool.exe').is_file(): + self.mutoolbin = application_path / 'mutool.exe' else: DEVNULL = open(os.devnull, 'wb') # First try to find mudraw @@ -143,11 +143,11 @@ class PdfController(PresentationController): self.gsbin = which('gs') # Last option: check if mudraw or mutool is placed in OpenLP base folder if not self.mudrawbin and not self.mutoolbin and not self.gsbin: - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw')): - self.mudrawbin = os.path.join(application_path, 'mudraw') - elif os.path.isfile(os.path.join(application_path, 'mutool')): - self.mutoolbin = os.path.join(application_path, 'mutool') + application_path = AppLocation.get_directory(AppLocation.AppDir) + if (application_path / 'mudraw').is_file(): + self.mudrawbin = application_path / 'mudraw' + elif (application_path / 'mutool').is_file(): + self.mutoolbin = application_path / 'mutool' if self.mudrawbin or self.mutoolbin: self.also_supports = ['xps', 'oxps'] return True @@ -172,12 +172,15 @@ class PdfDocument(PresentationDocument): image-serviceitem on the fly and present as such. Therefore some of the 'playback' functions is not implemented. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Pdf') - PresentationDocument.__init__(self, controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.blanked = False self.hidden = False @@ -200,13 +203,13 @@ class PdfDocument(PresentationDocument): :return: The resolution dpi to be used. """ # Use a postscript script to get size of the pdf. It is assumed that all pages have same size - gs_resolution_script = str(AppLocation.get_directory( - AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps' + gs_resolution_script = AppLocation.get_directory( + AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps' # Run the script on the pdf to get the size runlog = [] try: - runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', - '-sFile=' + self.file_path, gs_resolution_script], + runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', + '-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)], startupinfo=self.startupinfo) except CalledProcessError as e: log.debug(' '.join(e.cmd)) @@ -246,7 +249,7 @@ class PdfDocument(PresentationDocument): created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) + self.image_files.append(image_path) self.num_pages = len(self.image_files) return True size = ScreenList().current['size'] @@ -258,27 +261,27 @@ class PdfDocument(PresentationDocument): # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') - runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), '-h', str(size.height()), + '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.mutoolbin: log.debug('loading presentation using mutool') - runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', - str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()), + '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.gsbin: log.debug('loading presentation using gs') resolution = self.gs_get_resolution(size) - runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', - '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', - '-sOutputFile=' + str(temp_dir_path / 'mainslide%03d.png'), - self.file_path], startupinfo=self.startupinfo) + runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', + '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', + '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) - except Exception: + self.image_files.append(image_path) + except Exception as e: log.exception(runlog) return False self.num_pages = len(self.image_files) diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 1014db851..fa253ffda 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -120,15 +120,16 @@ class PowerpointDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. :param controller: - :param presentation: + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Powerpoint') - super(PowerpointDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.index_map = {} self.slide_count = 0 @@ -145,7 +146,7 @@ class PowerpointDocument(PresentationDocument): try: if not self.controller.process: self.controller.start_process() - self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False) + self.controller.process.Presentations.Open(str(self.file_path), False, False, False) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.create_thumbnails() self.create_titles_and_notes() @@ -363,9 +364,8 @@ class PowerpointDocument(PresentationDocument): width=size.width(), horizontal=(right - left))) log.debug('window title: {title}'.format(title=window_title)) - filename_root, filename_ext = os.path.splitext(os.path.basename(self.file_path)) if size.y() == top and size.height() == (bottom - top) and size.x() == left and \ - size.width() == (right - left) and filename_root in window_title: + size.width() == (right - left) and self.file_path.stem in window_title: log.debug('Found a match and will save the handle') self.presentation_hwnd = hwnd # Stop powerpoint from flashing in the taskbar diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index c936fe65c..547636026 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -85,9 +85,9 @@ class PptviewController(PresentationController): if self.process: return log.debug('start PPTView') - dll_path = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), - 'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll') - self.process = cdll.LoadLibrary(dll_path) + dll_path = AppLocation.get_directory(AppLocation.AppDir) \ + / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll' + self.process = cdll.LoadLibrary(str(dll_path)) if log.isEnabledFor(logging.DEBUG): self.process.SetDebug(1) @@ -104,12 +104,15 @@ class PptviewDocument(PresentationDocument): """ Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path to the document to load + :rtype: None """ log.debug('Init Presentation PowerPoint') - super(PptviewDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.ppt_id = None self.blanked = False @@ -121,17 +124,16 @@ class PptviewDocument(PresentationDocument): the background PptView task started earlier. """ log.debug('LoadPresentation') - temp_dir_path = self.get_temp_folder() + temp_path = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - self.file_path = os.path.normpath(self.file_path) - preview_path = temp_dir_path / 'slide' + preview_path = temp_path / 'slide' # Ensure that the paths are null terminated - byte_file_path = self.file_path.encode('utf-16-le') + b'\0' - preview_file_name = str(preview_path).encode('utf-16-le') + b'\0' - if not temp_dir_path: - temp_dir_path.mkdir(parents=True) - self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_file_name) + file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0' + preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0' + if not temp_path.is_dir(): + temp_path.mkdir(parents=True) + self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() @@ -148,7 +150,7 @@ class PptviewDocument(PresentationDocument): return log.debug('create_thumbnails proceeding') for idx in range(self.get_slide_count()): - path = '{folder}\\slide{index}.bmp'.format(folder=self.get_temp_folder(), index=str(idx + 1)) + path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=idx + 1) self.convert_thumbnail(path, idx + 1) def create_titles_and_notes(self): @@ -161,13 +163,12 @@ class PptviewDocument(PresentationDocument): """ titles = None notes = None - filename = os.path.normpath(self.file_path) # let's make sure we have a valid zipped presentation - if os.path.exists(filename) and zipfile.is_zipfile(filename): + if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)): namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} # open the file - with zipfile.ZipFile(filename) as zip_file: + with zipfile.ZipFile(str(self.file_path)) as zip_file: # find the presentation.xml to get the slide count with zip_file.open('ppt/presentation.xml') as pres: tree = ElementTree.parse(pres) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index af665bb55..13a759a5e 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -19,10 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os -import shutil from PyQt5 import QtCore @@ -87,19 +84,26 @@ class PresentationDocument(object): Returns a path to an image containing a preview for the requested slide """ - def __init__(self, controller, name): + def __init__(self, controller, document_path): """ Constructor for the PresentationController class + + :param controller: + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.controller = controller - self._setup(name) + self._setup(document_path) - def _setup(self, name): + def _setup(self, document_path): """ Run some initial setup. This method is separate from __init__ in order to mock it out in tests. + + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.slide_number = 0 - self.file_path = name + self.file_path = document_path check_directory_exists(self.get_thumbnail_folder()) def load_presentation(self): @@ -126,12 +130,6 @@ class PresentationDocument(object): except OSError: log.exception('Failed to delete presentation controller files') - def get_file_name(self): - """ - Return just the filename of the presentation, without the directory - """ - return os.path.split(self.file_path)[1] - def get_thumbnail_folder(self): """ The location where thumbnail images will be stored @@ -141,9 +139,9 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.thumbnail_folder, folder) def get_temp_folder(self): @@ -155,19 +153,22 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ - Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file. + Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file. + + :return: If the thumbnail is valid + :rtype: bool """ - last_image = self.get_thumbnail_path(self.get_slide_count(), True) - if not (last_image and os.path.isfile(last_image)): + last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) + if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(self.file_path, last_image) + return validate_thumb(self.file_path, last_image_path) def close_presentation(self): """ @@ -250,24 +251,28 @@ class PresentationDocument(object): """ pass - def convert_thumbnail(self, file, idx): + def convert_thumbnail(self, image_path, index): """ Convert the slide image the application made to a scaled 360px height .png image. + + :param openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of + :param int index: The index of the slide to create the thumbnail for. + :rtype: None """ if self.check_thumbnails(): return - if os.path.isfile(file): - thumb_path = self.get_thumbnail_path(idx, False) - create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) + if image_path.is_file(): + thumb_path = self.get_thumbnail_path(index, False) + create_thumb(str(image_path), str(thumb_path), False, QtCore.QSize(-1, 360)) - def get_thumbnail_path(self, slide_no, check_exists=True): + def get_thumbnail_path(self, slide_no, check_exists=False): """ Returns an image path containing a preview for the requested slide :param int slide_no: The slide an image is required for, starting at 1 :param bool check_exists: Check if the generated path exists :return: The path, or None if the :param:`check_exists` is True and the file does not exist - :rtype: openlp.core.common.path.Path, None + :rtype: openlp.core.common.path.Path | None """ path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png') if path.is_file() or not check_exists: @@ -313,43 +318,38 @@ class PresentationDocument(object): Reads the titles from the titles file and the notes files and returns the content in two lists """ - titles = [] notes = [] - titles_file = str(self.get_thumbnail_folder() / 'titles.txt') - if os.path.exists(titles_file): - try: - with open(titles_file, encoding='utf-8') as fi: - titles = fi.read().splitlines() - except: - log.exception('Failed to open/read existing titles file') - titles = [] + titles_path = self.get_thumbnail_folder() / 'titles.txt' + try: + titles = titles_path.read_text().splitlines() + except: + log.exception('Failed to open/read existing titles file') + titles = [] for slide_no, title in enumerate(titles, 1): - notes_file = str(self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)) - note = '' - if os.path.exists(notes_file): - try: - with open(notes_file, encoding='utf-8') as fn: - note = fn.read() - except: - log.exception('Failed to open/read notes file') - note = '' + notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) + try: + note = notes_path.read_text() + except: + log.exception('Failed to open/read notes file') + note = '' notes.append(note) return titles, notes def save_titles_and_notes(self, titles, notes): """ - Performs the actual persisting of titles to the titles.txt - and notes to the slideNote%.txt + Performs the actual persisting of titles to the titles.txt and notes to the slideNote%.txt + + :param list[str] titles: The titles to save + :param list[str] notes: The notes to save + :rtype: None """ if titles: titles_path = self.get_thumbnail_folder() / 'titles.txt' - with titles_path.open(mode='wt', encoding='utf-8') as fo: - fo.writelines(titles) + titles_path.write_text('\n'.join(titles)) if notes: for slide_no, note in enumerate(notes, 1): notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) - with notes_path.open(mode='wt', encoding='utf-8') as fn: - fn.write(note) + notes_path.write_text(note) class PresentationController(object): @@ -426,12 +426,11 @@ class PresentationController(object): self.document_class = document_class self.settings_section = self.plugin.settings_section self.available = None - self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name) - self.thumbnail_folder = os.path.join( - str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') + self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name + self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails' self.thumbnail_prefix = 'slide' - check_directory_exists(Path(self.thumbnail_folder)) - check_directory_exists(Path(self.temp_folder)) + check_directory_exists(self.thumbnail_folder) + check_directory_exists(self.temp_folder) def enabled(self): """ @@ -466,11 +465,15 @@ class PresentationController(object): log.debug('Kill') self.close_presentation() - def add_document(self, name): + def add_document(self, document_path): """ Called when a new presentation document is opened. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :return: The document + :rtype: PresentationDocument """ - document = self.document_class(self, name) + document = self.document_class(self, document_path) self.docs.append(document) return document diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 3e92827c8..ca9ceacbc 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -38,7 +38,6 @@ class PresentationTab(SettingsTab): """ Constructor """ - self.parent = parent self.controllers = controllers super(PresentationTab, self).__init__(parent, title, visible_title, icon_path) self.activated = False @@ -194,7 +193,7 @@ class PresentationTab(SettingsTab): pdf_program_path = self.program_path_edit.path enable_pdf_program = self.pdf_program_check_box.checkState() # If the given program is blank disable using the program - if not pdf_program_path: + if pdf_program_path is None: enable_pdf_program = 0 if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'): Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path) @@ -220,9 +219,11 @@ class PresentationTab(SettingsTab): def on_program_path_edit_path_changed(self, new_path): """ - Select the mudraw or ghostscript binary that should be used. + Handle the `pathEditChanged` signal from program_path_edit + + :param openlp.core.common.path.Path new_path: File path to the new program + :rtype: None """ - new_path = path_to_str(new_path) if new_path: if not PdfController.process_check_binary(new_path): critical_error_message_box(UiStrings().Error, diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 1bbd29522..c5e6d3df3 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -244,20 +244,3 @@ class TestPresentationDocument(TestCase): # THEN: load_presentation should return false self.assertFalse(result, "PresentationDocument.load_presentation should return false.") - - def test_get_file_name(self): - """ - Test the PresentationDocument.get_file_name method. - """ - - # GIVEN: A mocked os.path.split which returns a list, an instance of PresentationDocument and - # arbitary file_path. - self.mock_os.path.split.return_value = ['directory', 'file.ext'] - instance = PresentationDocument(self.mock_controller, 'Name') - instance.file_path = 'filepath' - - # WHEN: Calling get_file_name - result = instance.get_file_name() - - # THEN: get_file_name should return 'file.ext' - self.assertEqual(result, 'file.ext') From 7f98003d5492603c62db5fac1d1c000ebef4d442 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sun, 17 Sep 2017 20:43:15 +0100 Subject: [PATCH 10/52] test fixes --- openlp/core/lib/__init__.py | 11 ++-- openlp/core/ui/thememanager.py | 2 +- openlp/plugins/images/lib/mediaitem.py | 2 +- openlp/plugins/presentations/lib/mediaitem.py | 2 +- .../presentations/lib/pdfcontroller.py | 3 +- .../lib/presentationcontroller.py | 2 +- tests/functional/openlp_core_lib/test_lib.py | 55 ++++++---------- .../presentations/test_impresscontroller.py | 7 +- .../presentations/test_mediaitem.py | 19 +++--- .../presentations/test_pdfcontroller.py | 15 +++-- .../presentations/test_pptviewcontroller.py | 14 ++-- .../test_presentationcontroller.py | 65 +++++++------------ 12 files changed, 83 insertions(+), 114 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 0babbc0d1..1d55df497 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -29,7 +29,7 @@ import os import re import math -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtGui, Qt, QtWidgets from openlp.core.common import translate from openlp.core.common.path import Path @@ -221,12 +221,11 @@ def validate_thumb(file_path, thumb_path): Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function, before checking the existence of the file. - :param file_path: The path to the file. The file **must** exist! - :param thumb_path: The path to the thumb. - :return: True, False if the image has changed since the thumb was created. + :param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist! + :param openlp.core.common.path.Path thumb_path: The path to the thumb. + :return: Has the image changed since the thumb was created? + :rtype: bool """ - file_path = Path(file_path) - thumb_path = Path(thumb_path) if not thumb_path.exists(): return False image_date = file_path.stat().st_mtime diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 61381a902..15e33cdb2 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -483,7 +483,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage name = text_name thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name)) item_name = QtWidgets.QListWidgetItem(name) - if validate_thumb(theme, thumb): + if validate_thumb(Path(theme), Path(thumb)): icon = build_icon(thumb) else: icon = create_thumb(theme, thumb) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index bcf222eb0..d1ea2003f 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -360,7 +360,7 @@ class ImageMediaItem(MediaManagerItem): if not os.path.exists(image_file.filename): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(image_file.filename, thumb): + if validate_thumb(Path(image_file.filename), Path(thumb)): icon = build_icon(thumb) else: icon = create_thumb(image_file.filename, thumb) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index aa5bfc0d6..d9a14e0ed 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -197,7 +197,7 @@ class PresentationMediaItem(MediaManagerItem): if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(preview_path, thumbnail_path): + if validate_thumb(Path(preview_path), Path(thumbnail_path)): icon = build_icon(thumbnail_path) else: icon = create_thumb(str(preview_path), str(thumbnail_path)) diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 9f4aa1b4f..a39cce36c 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -261,7 +261,8 @@ class PdfDocument(PresentationDocument): # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') - runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), '-h', str(size.height()), + runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), + '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.mutoolbin: diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 13a759a5e..3225eac24 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -168,7 +168,7 @@ class PresentationDocument(object): last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(self.file_path, last_image_path) + return validate_thumb(Path(self.file_path), Path(last_image_path)) def close_presentation(self): """ diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 2056665f4..8b46e99c3 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -595,61 +595,46 @@ class TestLib(TestCase): Test the validate_thumb() function when the thumbnail does not exist """ # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - mocked_os.path.exists.return_value = False + with patch.object(Path, 'exists', return_value=False) as mocked_path_exists: + file_path = Path('path', 'to', 'file') + thumb_path = Path('path', 'to', 'thumb') # WHEN: we run the validate_thumb() function result = validate_thumb(file_path, thumb_path) # THEN: we should have called a few functions, and the result should be False - mocked_os.path.exists.assert_called_with(thumb_path) - assert result is False, 'The result should be False' + thumb_path.exists.assert_called_once_with() + self.assertFalse(result, 'The result should be False') def test_validate_thumb_file_exists_and_newer(self): """ Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file """ - # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - file_mocked_stat = MagicMock() - file_mocked_stat.st_mtime = datetime.now() - thumb_mocked_stat = MagicMock() - thumb_mocked_stat.st_mtime = datetime.now() + timedelta(seconds=10) - mocked_os.path.exists.return_value = True - mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat] + with patch.object(Path, 'exists'), patch.object(Path, 'stat'): + # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times + file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)}) + thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)}) # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) - # THEN: we should have called a few functions, and the result should be True - # mocked_os.path.exists.assert_called_with(thumb_path) + # THEN: `validate_thumb` should return True + self.assertTrue(result) def test_validate_thumb_file_exists_and_older(self): """ Test the validate_thumb() function when the thumbnail exists but is older than the file """ - # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - file_mocked_stat = MagicMock() - file_mocked_stat.st_mtime = datetime.now() - thumb_mocked_stat = MagicMock() - thumb_mocked_stat.st_mtime = datetime.now() - timedelta(seconds=10) - mocked_os.path.exists.return_value = True - mocked_os.stat.side_effect = lambda fname: file_mocked_stat if fname == file_path else thumb_mocked_stat + # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times + file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)}) + thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)}) - # WHEN: we run the validate_thumb() function - result = validate_thumb(file_path, thumb_path) + # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) - # THEN: we should have called a few functions, and the result should be False - mocked_os.path.exists.assert_called_with(thumb_path) - mocked_os.stat.assert_any_call(file_path) - mocked_os.stat.assert_any_call(thumb_path) - assert result is False, 'The result should be False' + # THEN: `validate_thumb` should return False + thumb_path.stat.assert_called_once_with() + self.assertFalse(result, 'The result should be False') def test_replace_params_no_params(self): """ diff --git a/tests/functional/openlp_plugins/presentations/test_impresscontroller.py b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py index d383b16e4..a792988e2 100644 --- a/tests/functional/openlp_plugins/presentations/test_impresscontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py @@ -24,13 +24,12 @@ Functional tests to test the Impress class and related methods. """ from unittest import TestCase from unittest.mock import MagicMock -import os import shutil from tempfile import mkdtemp from openlp.core.common import Settings -from openlp.plugins.presentations.lib.impresscontroller import \ - ImpressController, ImpressDocument, TextType +from openlp.core.common.path import Path +from openlp.plugins.presentations.lib.impresscontroller import ImpressController, ImpressDocument, TextType from openlp.plugins.presentations.presentationplugin import __default_settings__ from tests.utils.constants import TEST_RESOURCES_PATH @@ -82,7 +81,7 @@ class TestImpressDocument(TestCase): mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' Settings().extend_default_settings(__default_settings__) - self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + self.file_name = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') self.ppc = ImpressController(mocked_plugin) self.doc = ImpressDocument(self.ppc, self.file_name) diff --git a/tests/functional/openlp_plugins/presentations/test_mediaitem.py b/tests/functional/openlp_plugins/presentations/test_mediaitem.py index b5299d785..9ce0a5fdc 100644 --- a/tests/functional/openlp_plugins/presentations/test_mediaitem.py +++ b/tests/functional/openlp_plugins/presentations/test_mediaitem.py @@ -26,6 +26,7 @@ from unittest import TestCase from unittest.mock import patch, MagicMock, call from openlp.core.common import Registry +from openlp.core.common.path import Path from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem from tests.helpers.testmixin import TestMixin @@ -92,17 +93,18 @@ class TestMediaItem(TestCase, TestMixin): """ # GIVEN: A mocked controller, and mocked os.path.getmtime mocked_controller = MagicMock() - mocked_doc = MagicMock() + mocked_doc = MagicMock(**{'get_thumbnail_path.return_value': Path()}) mocked_controller.add_document.return_value = mocked_doc mocked_controller.supports = ['tmp'] self.media_item.controllers = { 'Mocked': mocked_controller } - presentation_file = 'file.tmp' - with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime, \ - patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: - mocked_getmtime.side_effect = [100, 200] - mocked_exists.return_value = True + + thmub_path = MagicMock(st_mtime=100) + file_path = MagicMock(st_mtime=400) + with patch.object(Path, 'stat', side_effect=[thmub_path, file_path]), \ + patch.object(Path, 'exists', return_value=True): + presentation_file = Path('file.tmp') # WHEN: calling clean_up_thumbnails self.media_item.clean_up_thumbnails(presentation_file, True) @@ -123,9 +125,8 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.controllers = { 'Mocked': mocked_controller } - presentation_file = 'file.tmp' - with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: - mocked_exists.return_value = False + presentation_file = Path('file.tmp') + with patch.object(Path, 'exists', return_value=False): # WHEN: calling clean_up_thumbnails self.media_item.clean_up_thumbnails(presentation_file, True) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index be4aeeaa4..25a8394f0 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -32,6 +32,7 @@ from PyQt5 import QtCore, QtGui from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument from openlp.core.common import Settings +from openlp.core.common.path import Path from openlp.core.lib import ScreenList from tests.utils.constants import TEST_RESOURCES_PATH @@ -66,8 +67,8 @@ class TestPdfController(TestCase, TestMixin): self.desktop.screenGeometry.return_value = SCREEN['size'] self.screens = ScreenList.create(self.desktop) Settings().extend_default_settings(__default_settings__) - self.temp_folder = mkdtemp() - self.thumbnail_folder = mkdtemp() + self.temp_folder = Path(mkdtemp()) + self.thumbnail_folder = Path(mkdtemp()) self.mock_plugin = MagicMock() self.mock_plugin.settings_section = self.temp_folder @@ -77,8 +78,8 @@ class TestPdfController(TestCase, TestMixin): """ del self.screens self.destroy_settings() - shutil.rmtree(self.thumbnail_folder) - shutil.rmtree(self.temp_folder) + shutil.rmtree(str(self.thumbnail_folder)) + shutil.rmtree(str(self.temp_folder)) def test_constructor(self): """ @@ -98,7 +99,7 @@ class TestPdfController(TestCase, TestMixin): Test loading of a Pdf using the PdfController """ # GIVEN: A Pdf-file - test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') + test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') # WHEN: The Pdf is loaded controller = PdfController(plugin=self.mock_plugin) @@ -118,7 +119,7 @@ class TestPdfController(TestCase, TestMixin): Test loading of a Pdf and check size of generate pictures """ # GIVEN: A Pdf-file - test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') + test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') # WHEN: The Pdf is loaded controller = PdfController(plugin=self.mock_plugin) @@ -131,7 +132,7 @@ class TestPdfController(TestCase, TestMixin): # THEN: The load should succeed and pictures should be created and have been scales to fit the screen self.assertTrue(loaded, 'The loading of the PDF should succeed.') - image = QtGui.QImage(os.path.join(self.temp_folder, 'pdf_test1.pdf', 'mainslide001.png')) + image = QtGui.QImage(os.path.join(str(self.temp_folder), 'pdf_test1.pdf', 'mainslide001.png')) # Based on the converter used the resolution will differ a bit if controller.gsbin: self.assertEqual(760, image.height(), 'The height should be 760') diff --git a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py index 3c08d226a..bfa74a7fa 100644 --- a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py @@ -22,7 +22,6 @@ """ This module contains tests for the pptviewcontroller module of the Presentations plugin. """ -import os import shutil from tempfile import mkdtemp from unittest import TestCase @@ -30,6 +29,7 @@ from unittest.mock import MagicMock, patch from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController from openlp.core.common import is_win +from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin from tests.utils.constants import TEST_RESOURCES_PATH @@ -184,7 +184,7 @@ class TestPptviewDocument(TestCase): """ # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') doc.save_titles_and_notes = MagicMock() # WHEN reading the titles and notes @@ -201,13 +201,13 @@ class TestPptviewDocument(TestCase): """ # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file with patch('builtins.open') as mocked_open, \ - patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \ + patch.object(Path, 'exists') as mocked_path_exists, \ patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \ mocked_dir_exists: - mocked_exists.return_value = False + mocked_path_exists.return_value = False mocked_dir_exists.return_value = False doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = 'Idontexist.pptx' + doc.file_path = Path('Idontexist.pptx') doc.save_titles_and_notes = MagicMock() # WHEN: Reading the titles and notes @@ -215,7 +215,7 @@ class TestPptviewDocument(TestCase): # THEN: File existens should have been checked, and not have been opened. doc.save_titles_and_notes.assert_called_once_with(None, None) - mocked_exists.assert_any_call('Idontexist.pptx') + mocked_path_exists.assert_called_with() self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.') def test_create_titles_and_notes_invalid_file(self): @@ -228,7 +228,7 @@ class TestPptviewDocument(TestCase): mocked_is_zf.return_value = False mocked_open.filesize = 10 doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') + doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') doc.save_titles_and_notes = MagicMock() # WHEN: reading the titles and notes diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index c5e6d3df3..36ceb6f43 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -23,9 +23,8 @@ Functional tests to test the PresentationController and PresentationDocument classes and related methods. """ -import os from unittest import TestCase -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import MagicMock, call, patch from openlp.core.common.path import Path from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument @@ -40,7 +39,7 @@ class TestPresentationController(TestCase): def setUp(self): self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder', - return_value=Path()) + return_value=Path()) self.get_thumbnail_folder_patcher.start() mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' @@ -67,23 +66,18 @@ class TestPresentationController(TestCase): Test PresentationDocument.save_titles_and_notes method with two valid lists """ # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder - mocked_open = mock_open() - with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.write_text') as mocked_write_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: titles = ['uno', 'dos'] notes = ['one', 'two'] # WHEN: calling save_titles_and_notes - mocked_get_thumbnail_folder.return_value = 'test' + mocked_get_thumbnail_folder.return_value = Path('test') self.document.save_titles_and_notes(titles, notes) # THEN: the last call to open should have been for slideNotes2.txt - mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='wt', encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='wt', encoding='utf-8') - self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened') - mocked_open().writelines.assert_called_once_with(['uno', 'dos']) - mocked_open().write.assert_any_call('one') - mocked_open().write.assert_any_call('two') + self.assertEqual(mocked_write_text.call_count, 3, 'There should be exactly three files written') + mocked_write_text.assert_has_calls([call('uno\ndos'), call('one'), call('two')]) def test_save_titles_and_notes_with_None(self): """ @@ -107,10 +101,11 @@ class TestPresentationController(TestCase): """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \ + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text', + return_value='uno\ndos\n') as mocked_read_text, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' + patch('openlp.plugins.presentations.lib.presentationcontroller.Path.exists') as mocked_exists: + mocked_get_thumbnail_folder.return_value = Path('test') mocked_exists.return_value = True # WHEN: calling get_titles_and_notes @@ -121,45 +116,36 @@ class TestPresentationController(TestCase): self.assertEqual(len(result_titles), 2, 'There should be two items in the titles') self.assertIs(type(result_notes), list, 'result_notes should be of type list') self.assertEqual(len(result_notes), 2, 'There should be two items in the notes') - self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened') - mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), encoding='utf-8') - self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked') + self.assertEqual(mocked_read_text.call_count, 3, 'Three files should be read') def test_get_titles_and_notes_with_file_not_found(self): """ Test PresentationDocument.get_titles_and_notes method with file not found """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open') as mocked_open, \ - patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' - mocked_exists.return_value = False + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + mocked_read_text.side_effect = FileNotFoundError() + mocked_get_thumbnail_folder.return_value = Path('test') # WHEN: calling get_titles_and_notes result_titles, result_notes = self.document.get_titles_and_notes() # THEN: it should return two empty lists - self.assertIs(type(result_titles), list, 'result_titles should be of type list') + self.assertIsInstance(result_titles, list, 'result_titles should be of type list') self.assertEqual(len(result_titles), 0, 'there be no titles') - self.assertIs(type(result_notes), list, 'result_notes should be a list') + self.assertIsInstance(result_notes, list, 'result_notes should be a list') self.assertEqual(len(result_notes), 0, 'but the list should be empty') - self.assertEqual(mocked_open.call_count, 0, 'No calls to open files') - self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists') def test_get_titles_and_notes_with_file_error(self): """ Test PresentationDocument.get_titles_and_notes method with file errors """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open') as mocked_open, \ - patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' - mocked_exists.return_value = True - mocked_open.side_effect = IOError() + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + mocked_read_text.side_effect = IOError() + mocked_get_thumbnail_folder.return_value = Path('test') # WHEN: calling get_titles_and_notes result_titles, result_notes = self.document.get_titles_and_notes() @@ -180,18 +166,16 @@ class TestPresentationDocument(TestCase): patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder') - self.os_patcher = patch('openlp.plugins.presentations.lib.presentationcontroller.os') self._setup_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup') self.mock_check_directory_exists = self.check_directory_exists_patcher.start() self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start() - self.mock_os = self.os_patcher.start() self.mock_setup = self._setup_patcher.start() self.mock_controller = MagicMock() - self.mock_get_thumbnail_folder.return_value = 'returned/path/' + self.mock_get_thumbnail_folder.return_value = Path('returned/path/') def tearDown(self): """ @@ -199,7 +183,6 @@ class TestPresentationDocument(TestCase): """ self.check_directory_exists_patcher.stop() self.get_thumbnail_folder_patcher.stop() - self.os_patcher.stop() self._setup_patcher.stop() def test_initialise_presentation_document(self): @@ -227,7 +210,7 @@ class TestPresentationDocument(TestCase): PresentationDocument(self.mock_controller, 'Name') # THEN: check_directory_exists should have been called with 'returned/path/' - self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path')) + self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path/')) self._setup_patcher.start() From d801ca9b09010e75769c994ea4e102163e9d4647 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 07:20:06 +0100 Subject: [PATCH 11/52] Test patched which method --- openlp/core/lib/shutil.py | 12 +- openlp/core/ui/servicemanager.py | 5 +- .../presentations/lib/impresscontroller.py | 8 +- .../functional/openlp_core_lib/test_shutil.py | 153 ++++++++++-------- 4 files changed, 97 insertions(+), 81 deletions(-) diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py index 44dea590a..1c7a9a393 100755 --- a/openlp/core/lib/shutil.py +++ b/openlp/core/lib/shutil.py @@ -95,19 +95,17 @@ def rmtree(*args, **kwargs): args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) return shutil.rmtree(*args, **kwargs) -# TODO: Test and tidy + + def which(*args, **kwargs): """ - Wraps :func:shutil.rmtree` so that we can accept Path objects. + Wraps :func:shutil.which` so that it return a Path objects. - :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object - :return: Passes the return from :func:`shutil.rmtree` back - :rtype: None + :rtype: openlp.core.common.Path See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.rmtree + https://docs.python.org/3/library/shutil.html#shutil.which """ - file_name = shutil.which(*args, **kwargs) if file_name: return str_to_path(file_name) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index eb279f267..b393ad736 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -376,7 +376,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self._file_name = path_to_str(file_path) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) Settings().setValue('servicemanager/last file', file_path) - self._save_lite = file_path.suffix() == '.oszl' + if file_path and file_path.suffix() == '.oszl': + self._save_lite = True + else: + self._save_lite = False def file_name(self): """ diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 472e07801..e4b45465c 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -223,10 +223,9 @@ class ImpressDocument(PresentationDocument): if desktop is None: self.controller.start_process() desktop = self.controller.get_com_desktop() - url = self.file_path.as_uri() else: desktop = self.controller.get_uno_desktop() - url = uno.systemPathToFileUrl(str(self.file_path)) + url = self.file_path.as_uri() if desktop is None: return False self.desktop = desktop @@ -253,10 +252,7 @@ class ImpressDocument(PresentationDocument): if self.check_thumbnails(): return temp_folder_path = self.get_temp_folder() - if is_win(): - thumb_dir_url = temp_folder_path.as_uri() - else: - thumb_dir_url = uno.systemPathToFileUrl(str(temp_folder_path)) + thumb_dir_url = temp_folder_path.as_uri() properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py index f502e403b..0f6ca9078 100755 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -3,15 +3,15 @@ from unittest import TestCase from unittest.mock import ANY, MagicMock, patch from openlp.core.common.path import Path -from openlp.core.lib import shutilpatches +from openlp.core.lib.shutil import copy, copyfile, copytree, rmtree, which -class TestShutilPatches(TestCase): +class TestShutil(TestCase): """ Tests for the :mod:`openlp.core.lib.shutil` module """ - def test_pcopy(self): + def test_copy(self): """ Test :func:`copy` """ @@ -19,133 +19,152 @@ class TestShutilPatches(TestCase): with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ as mocked_shutil_copy: - # WHEN: Calling shutilpatches.copy with the src and dst parameters as Path object types - result = shutilpatches.copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copy` with the src and dst parameters as Path object types + result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copy` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copy` should return the str type result of calling `shutil.copy` as a Path - # object. + # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. + # :func:`copy` should return the str type result of calling :func:`shutil.copy` as a Path object. mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopy_follow_optional_params(self): + def test_copy_follow_optional_params(self): """ Test :func:`copy` when follow_symlinks is set to false """ # GIVEN: A mocked `shutil.copy` with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: - # WHEN: Calling shutilpatches.copy with `follow_symlinks` set to False - shutilpatches.copy(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - follow_symlinks=False) + # WHEN: Calling :func:`copy` with :param:`follow_symlinks` set to False + copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - # THEN: `shutil.copy` should have been called with follow_symlinks is set to false + # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) - def test_pcopyfile(self): + def test_copyfile(self): """ Test :func:`copyfile` """ - # GIVEN: A mocked `shutil.copyfile` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copyfile', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copyfile: + # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copyfile', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: - # WHEN: Calling shutilpatches.copyfile with the src and dst parameters as Path object types - result = shutilpatches.copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copyfile` with the src and dst parameters as Path object types + result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copyfile` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copyfile` should return the str type result of calling `shutil.copyfile` as a Path + # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. + # :func:`copyfile` should return the str type result of calling :func:`shutil.copyfile` as a Path # object. mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopyfile_optional_params(self): + def test_copyfile_optional_params(self): """ Test :func:`copyfile` when follow_symlinks is set to false """ - # GIVEN: A mocked `shutil.copyfile` + # GIVEN: A mocked :func:`shutil.copyfile` with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: - # WHEN: Calling shutilpatches.copyfile with `follow_symlinks` set to False - shutilpatches.copyfile(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - follow_symlinks=False) + # WHEN: Calling :func:`copyfile` with :param:`follow_symlinks` set to False + copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - # THEN: `shutil.copyfile` should have been called with the optional parameters, with out any of the values - # being modified + # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the + # values being modified mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) - def test_pcopytree(self): + def test_copytree(self): """ Test :func:`copytree` """ - # GIVEN: A mocked `shutil.copytree` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copytree', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copytree: + # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copytree', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: - # WHEN: Calling shutilpatches.copytree with the src and dst parameters as Path object types - result = shutilpatches.copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copytree` with the src and dst parameters as Path object types + result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copytree` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copytree` should return the str type result of calling `shutil.copytree` as a Path - # object. + # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. + # :func:`patches.copytree` should return the str type result of calling :func:`shutil.copytree` as a + # Path object. mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopytree_optional_params(self): + def test_copytree_optional_params(self): """ Test :func:`copytree` when optional parameters are passed """ - # GIVEN: A mocked `shutil.copytree` + # GIVEN: A mocked :func:`shutil.copytree` with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: mocked_ignore = MagicMock() mocked_copy_function = MagicMock() - # WHEN: Calling shutilpatches.copytree with the optional parameters set - shutilpatches.copytree(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - symlinks=True, - ignore=mocked_ignore, - copy_function=mocked_copy_function, - ignore_dangling_symlinks=True) + # WHEN: Calling :func:`copytree` with the optional parameters set + copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, + ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - # THEN: `shutil.copytree` should have been called with the optional parameters, with out any of the values - # being modified - mocked_shutil_copytree.assert_called_once_with(ANY, ANY, - symlinks=True, - ignore=mocked_ignore, + # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - def test_prmtree(self): + def test_rmtree(self): """ Test :func:`rmtree` """ - # GIVEN: A mocked `shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_rmtree: + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: - # WHEN: Calling shutilpatches.rmtree with the path parameter as Path object type - result = shutilpatches.rmtree(Path('test', 'path')) + # WHEN: Calling :func:`rmtree` with the path parameter as Path object type + result = rmtree(Path('test', 'path')) - # THEN: `shutil.rmtree` should have been called with the str equivalents of the Path object. - mocked_rmtree.assert_called_once_with(os.path.join('test', 'path')) + # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) self.assertIsNone(result) - def test_prmtree_optional_params(self): + def test_rmtree_optional_params(self): """ - Test :func:`rmtree` when optional parameters are passed + Test :func:`rmtree` when optional parameters are passed """ - # GIVEN: A mocked `shutil.rmtree` + # GIVEN: A mocked :func:`shutil.rmtree` with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: mocked_on_error = MagicMock() - # WHEN: Calling shutilpatches.rmtree with `ignore_errors` set to True and `onerror` set to a mocked object - shutilpatches.rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + # WHEN: Calling :func:`rmtree` with :param:`ignore_errors` set to True and `onerror` set to a mocked object + rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) - # THEN: `shutil.rmtree` should have been called with the optional parameters, with out any of the values - # being modified + # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the + # values being modified mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) + + def test_which_no_command(self): + """ + Test :func:`which` when the command is not found. + """ + # GIVEN: A mocked :func:``shutil.which` when the command is not found. + with patch('openlp.core.lib.shutil.shutil.which', return_value=None) as mocked_shutil_which: + + # WHEN: Calling :func:`which` with a command that does not exist. + result = which('no_command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. + mocked_shutil_which.assert_called_once_with('no_command') + self.assertIsNone(result) + + def test_which_command(self): + """ + Test :func:`which` when a command has been found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is found. + with patch('openlp.core.lib.shutil.shutil.which', + return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: + + # WHEN: Calling :func:`which` with a command that exists. + result = which('command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a + # Path object equivalent of the command path. + mocked_shutil_which.assert_called_once_with('command') + self.assertEqual(result, Path('path', 'to', 'command')) From 0ee8ebb1c2f58cb4713d1c27456d6e0f01ba24f5 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 07:32:19 +0100 Subject: [PATCH 12/52] PEP fixes --- tests/functional/openlp_core_lib/test_shutil.py | 8 ++++---- .../presentations/test_presentationcontroller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py index 0f6ca9078..737f7ce00 100755 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -13,7 +13,7 @@ class TestShutil(TestCase): def test_copy(self): """ - Test :func:`copy` + Test :func:`copy` """ # GIVEN: A mocked `shutil.copy` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ @@ -43,7 +43,7 @@ class TestShutil(TestCase): def test_copyfile(self): """ - Test :func:`copyfile` + Test :func:`copyfile` """ # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copyfile', @@ -75,7 +75,7 @@ class TestShutil(TestCase): def test_copytree(self): """ - Test :func:`copytree` + Test :func:`copytree` """ # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copytree', @@ -112,7 +112,7 @@ class TestShutil(TestCase): def test_rmtree(self): """ - Test :func:`rmtree` + Test :func:`rmtree` """ # GIVEN: A mocked :func:`shutil.rmtree` with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 36ceb6f43..4cf2a1a01 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -39,7 +39,7 @@ class TestPresentationController(TestCase): def setUp(self): self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder', - return_value=Path()) + return_value=Path()) self.get_thumbnail_folder_patcher.start() mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' From 92c6b9c09dd2a5d93b1211d850c30165ac94b3af Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 21:08:28 +0100 Subject: [PATCH 13/52] Revert some requested changes --- openlp/core/common/registry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 33afe6f21..1894ac458 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -138,8 +138,11 @@ class Registry(object): if result: results.append(result) except TypeError: + # Who has called me can help in debugging + trace_error_handler(log) log.exception('Exception for function {function}'.format(function=function)) else: + trace_error_handler(log) log.exception('Event {event} called but not registered'.format(event=event)) return results From 15c8023357adf9dd82a032298e5accfedf43b640 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 19 Sep 2017 09:48:34 -0700 Subject: [PATCH 14/52] Moving over to requests --- openlp/core/api/poll.py | 2 +- openlp/core/common/httputils.py | 177 +++++------------- openlp/core/ui/firsttimeform.py | 20 +- openlp/core/version.py | 1 - openlp/plugins/bibles/lib/importers/http.py | 28 +-- openlp/plugins/remotes/deploy.py | 4 +- .../openlp_core_api/test_websockets.py | 13 +- .../openlp_core_common/test_httputils.py | 30 +-- 8 files changed, 84 insertions(+), 191 deletions(-) diff --git a/openlp/core/api/poll.py b/openlp/core/api/poll.py index b8a29e1f2..c4b25848a 100644 --- a/openlp/core/api/poll.py +++ b/openlp/core/api/poll.py @@ -52,7 +52,7 @@ class Poller(RegistryProperties): 'isSecure': Settings().value('api/authentication enabled'), 'isAuthorised': False, 'chordNotation': Settings().value('songs/chord notation'), - 'isStagedActive': self.is_stage_active(), + 'isStageActive': self.is_stage_active(), 'isLiveActive': self.is_live_active(), 'isChordsActive': self.is_chords_active() } diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index b0c9c1b2f..0a5c962f8 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -25,17 +25,12 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. import hashlib import logging import os -import platform -import socket import sys -import subprocess import time -import urllib.error -import urllib.parse -import urllib.request -from http.client import HTTPException from random import randint +import requests + from openlp.core.common import Registry, trace_error_handler log = logging.getLogger(__name__ + '.__init__') @@ -69,33 +64,6 @@ CONNECTION_TIMEOUT = 30 CONNECTION_RETRIES = 2 -class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): - """ - Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 - (Redirecting to urls with special chars) - """ - def redirect_request(self, req, fp, code, msg, headers, new_url): - # - """ - Test if the new_url can be decoded to ascii - - :param req: - :param fp: - :param code: - :param msg: - :param headers: - :param new_url: - :return: - """ - try: - new_url.encode('latin1').decode('ascii') - fixed_url = new_url - except Exception: - # The url could not be decoded to ascii, so we do some url encoding - fixed_url = urllib.parse.quote(new_url.encode('latin1').decode('utf-8', 'replace'), safe='/:') - return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) - - def get_user_agent(): """ Return a user agent customised for the platform the user is on. @@ -107,7 +75,7 @@ def get_user_agent(): return browser_list[random_index] -def get_web_page(url, header=None, update_openlp=False): +def get_web_page(url, headers=None, update_openlp=False, proxies=None): """ Attempts to download the webpage at url and returns that page or None. @@ -116,71 +84,37 @@ def get_web_page(url, header=None, update_openlp=False): :param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded. Defaults to False. """ - # TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a - # proxy_handler, build into an opener and install the opener into urllib2. - # http://docs.python.org/library/urllib2.html if not url: return None - # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 - opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) - urllib.request.install_opener(opener) - req = urllib.request.Request(url) - if not header or header[0].lower() != 'user-agent': - user_agent = get_user_agent() - req.add_header('User-Agent', user_agent) - if header: - req.add_header(header[0], header[1]) + if headers and 'user-agent' not in [key.lower() for key in headers.keys()]: + headers['User-Agent'] = get_user_agent() log.debug('Downloading URL = %s' % url) retries = 0 - while retries <= CONNECTION_RETRIES: - retries += 1 - time.sleep(0.1) + while retries < CONNECTION_RETRIES: + # Put this at the bottom + # retries += 1 + # time.sleep(0.1) try: - page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT) - log.debug('Downloaded page {text}'.format(text=page.geturl())) + response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT)) + log.debug('Downloaded page {url}'.format(url=response.url)) break - except urllib.error.URLError as err: - log.exception('URLError on {text}'.format(text=url)) - log.exception('URLError: {text}'.format(text=err.reason)) - page = None + except IOError: + # For now, catch IOError. All requests errors inherit from IOError + log.exception('Unable to connect to {url}'.format(url=url)) + response = None if retries > CONNECTION_RETRIES: - raise - except socket.timeout: - log.exception('Socket timeout: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except socket.gaierror: - log.exception('Socket gaierror: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except ConnectionRefusedError: - log.exception('ConnectionRefused: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - break - except ConnectionError: - log.exception('Connection error: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except HTTPException: - log.exception('HTTPException error: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise + raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url)) except: # Don't know what's happening, so reraise the original + log.exception('Unknown error when trying to connect to {url}'.format(url=url)) raise if update_openlp: Registry().get('application').process_events() - if not page: - log.exception('{text} could not be downloaded'.format(text=url)) + if not response or not response.text: + log.error('{url} could not be downloaded'.format(url=url)) return None - log.debug(page) - return page + log.debug(response.text) + return response.text def get_url_file_size(url): @@ -192,19 +126,18 @@ def get_url_file_size(url): retries = 0 while True: try: - site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - meta = site.info() - return int(meta.get("Content-Length")) - except urllib.error.URLError: + response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True) + return int(response.headers['Content-Length']) + except IOError: if retries > CONNECTION_RETRIES: - raise + raise ConnectionError('Unable to download {url}'.format(url=url)) else: retries += 1 time.sleep(0.1) continue -def url_get_file(callback, url, f_path, sha256=None): +def url_get_file(callback, url, file_path, sha256=None): """" Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any point. Returns False on download error. @@ -217,56 +150,42 @@ def url_get_file(callback, url, f_path, sha256=None): block_count = 0 block_size = 4096 retries = 0 - log.debug("url_get_file: " + url) - while True: + log.debug('url_get_file: %s', url) + while retries < CONNECTION_RETRIES: try: - filename = open(f_path, "wb") - url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - if sha256: - hasher = hashlib.sha256() - # Download until finished or canceled. - while not callback.was_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) + with open(file_path, 'wb') as saved_file: + response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True) if sha256: - hasher.update(data) - block_count += 1 - callback._download_progress(block_count, block_size) - filename.close() + hasher = hashlib.sha256() + # Download until finished or canceled. + for chunk in response.iter_content(chunk_size=block_size): + if callback.was_cancelled: + break + saved_file.write(chunk) + if sha256: + hasher.update(chunk) + block_count += 1 + callback._download_progress(block_count, block_size) + response.close() if sha256 and hasher.hexdigest() != sha256: - log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) - os.remove(f_path) + log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(), + sha256) + os.remove(file_path) return False - except (urllib.error.URLError, socket.timeout) as err: + break + except IOError: trace_error_handler(log) - filename.close() - os.remove(f_path) + os.remove(file_path) if retries > CONNECTION_RETRIES: return False else: retries += 1 time.sleep(0.1) continue - break # Delete file if cancelled, it may be a partial file. if callback.was_cancelled: - os.remove(f_path) + os.remove(file_path) return True -def ping(host): - """ - Returns True if host responds to a ping request - """ - # Ping parameters as function of OS - ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1" - args = "ping " + " " + ping_str + " " + host - need_sh = False if platform.system().lower() == "windows" else True - - # Ping - return subprocess.call(args, shell=need_sh) == 0 - - __all__ = ['get_web_page'] diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 1ae923467..a44384a61 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -181,22 +181,16 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.application.process_events() try: web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'), - header=('User-Agent', user_agent)) - except (urllib.error.URLError, ConnectionError) as err: - msg = QtWidgets.QMessageBox() - title = translate('OpenLP.FirstTimeWizard', 'Network Error') - msg.setText('{title} {error}'.format(title=title, - error=err.code if hasattr(err, 'code') else '')) - msg.setInformativeText(translate('OpenLP.FirstTimeWizard', - 'There was a network error attempting to ' - 'connect to retrieve initial configuration information')) - msg.setStandardButtons(msg.Ok) - ans = msg.exec() + headers={'User-Agent': user_agent}) + except ConnectionError: + QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'), + translate('OpenLP.FirstTimeWizard', 'There was a network error attempting ' + 'to connect to retrieve initial configuration information'), + QtWidgets.QMessageBox.Ok) web_config = False if web_config: - files = web_config.read() try: - self.config.read_string(files.decode()) + self.config.read_string(web_config) self.web = self.config.get('general', 'base url') self.songs_url = self.web + self.config.get('songs', 'directory') + '/' self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' diff --git a/openlp/core/version.py b/openlp/core/version.py index 9a532fcd8..14ddc40ff 100644 --- a/openlp/core/version.py +++ b/openlp/core/version.py @@ -139,7 +139,6 @@ def get_version(): global APPLICATION_VERSION if APPLICATION_VERSION: return APPLICATION_VERSION - print(sys.argv) if '--dev-version' in sys.argv or '-d' in sys.argv: # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied # there. diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index 1616ebcf7..c6dc90667 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -93,7 +93,7 @@ class BGExtract(RegistryProperties): NAME = 'BibleGateway' def __init__(self, proxy_url=None): - log.debug('BGExtract.init("{url}")'.format(url=proxy_url)) + log.debug('BGExtract.init(proxy_url="{url}")'.format(url=proxy_url)) self.proxy_url = proxy_url socket.setdefaulttimeout(30) @@ -285,15 +285,15 @@ class BGExtract(RegistryProperties): log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version)) url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)}) reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params) - page = get_web_page(reference_url) - if not page: + page_source = get_web_page(reference_url) + if not page_source: send_error_message('download') return None - page_source = page.read() - try: - page_source = str(page_source, 'utf8') - except UnicodeDecodeError: - page_source = str(page_source, 'cp1251') + # TODO: Is this even necessary anymore? + # try: + # page_source = str(page_source, 'utf8') + # except UnicodeDecodeError: + # page_source = str(page_source, 'cp1251') try: soup = BeautifulSoup(page_source, 'lxml') except Exception: @@ -759,7 +759,7 @@ class HTTPBible(BibleImport, RegistryProperties): return BiblesResourcesDB.get_verse_count(book_id, chapter) -def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None): +def get_soup_for_bible_ref(reference_url, headers=None, pre_parse_regex=None, pre_parse_substitute=None): """ Gets a webpage and returns a parsed and optionally cleaned soup or None. @@ -772,15 +772,15 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre if not reference_url: return None try: - page = get_web_page(reference_url, header, True) + page_source = get_web_page(reference_url, headers, update_openlp=True) except Exception as e: - page = None - if not page: + log.exception('Unable to download Bible %s, unknown exception occurred', reference_url) + page_source = None + if not page_source: send_error_message('download') return None - page_source = page.read() if pre_parse_regex and pre_parse_substitute is not None: - page_source = re.sub(pre_parse_regex, pre_parse_substitute, page_source.decode()) + page_source = re.sub(pre_parse_regex, pre_parse_substitute, page_source) soup = None try: soup = BeautifulSoup(page_source, 'lxml') diff --git a/openlp/plugins/remotes/deploy.py b/openlp/plugins/remotes/deploy.py index d971499f0..d159c9b16 100644 --- a/openlp/plugins/remotes/deploy.py +++ b/openlp/plugins/remotes/deploy.py @@ -49,10 +49,10 @@ def download_sha256(): user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() try: web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'), - header=('User-Agent', user_agent)) + headers={'User-Agent': user_agent}) except (urllib.error.URLError, ConnectionError) as err: return False - file_bits = web_config.read().decode('utf-8').split() + file_bits = web_config.split() return file_bits[0], file_bits[2] diff --git a/tests/functional/openlp_core_api/test_websockets.py b/tests/functional/openlp_core_api/test_websockets.py index 8f1356566..c66b73c64 100644 --- a/tests/functional/openlp_core_api/test_websockets.py +++ b/tests/functional/openlp_core_api/test_websockets.py @@ -70,7 +70,7 @@ class TestWSServer(TestCase, TestMixin): """ # GIVEN: A new httpserver # WHEN: I start the server - server = WebSocketServer() + WebSocketServer() # THEN: the api environment should have been created self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once') @@ -93,7 +93,7 @@ class TestWSServer(TestCase, TestMixin): """ Test the poll function returns the correct JSON """ - # WHEN: the system is configured with a set of data + # GIVEN: the system is configured with a set of data mocked_service_manager = MagicMock() mocked_service_manager.service_id = 21 mocked_live_controller = MagicMock() @@ -105,8 +105,15 @@ class TestWSServer(TestCase, TestMixin): mocked_live_controller.desktop_screen.isChecked.return_value = False Registry().register('live_controller', mocked_live_controller) Registry().register('service_manager', mocked_service_manager) + # WHEN: The poller polls + with patch.object(self.poll, 'is_stage_active') as mocked_is_stage_active, \ + patch.object(self.poll, 'is_live_active') as mocked_is_live_active, \ + patch.object(self.poll, 'is_chords_active') as mocked_is_chords_active: + mocked_is_stage_active.return_value = True + mocked_is_live_active.return_value = True + mocked_is_chords_active.return_value = True + poll_json = self.poll.poll() # THEN: the live json should be generated and match expected results - poll_json = self.poll.poll() self.assertTrue(poll_json['results']['blank'], 'The blank return value should be True') self.assertFalse(poll_json['results']['theme'], 'The theme return value should be False') self.assertFalse(poll_json['results']['display'], 'The display return value should be False') diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 26ac297dd..9cb32db25 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -28,7 +28,7 @@ import socket from unittest import TestCase from unittest.mock import MagicMock, patch -from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping +from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file from tests.helpers.testmixin import TestMixin @@ -253,7 +253,7 @@ class TestHttpUtils(TestCase, TestMixin): fake_url = 'this://is.a.fake/url' # WHEN: The get_url_file_size() method is called - size = get_url_file_size(fake_url) + get_url_file_size(fake_url) # THEN: The correct methods are called with the correct arguments and a web page is returned mock_urlopen.assert_called_with(fake_url, timeout=30) @@ -272,29 +272,3 @@ class TestHttpUtils(TestCase, TestMixin): # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') - - def test_ping_valid(self): - """ - Test ping for OpenLP - """ - # GIVEN: a valid url to test - url = "openlp.io" - - # WHEN: Attempt to check the url exists - url_found = ping(url) - - # THEN: It should be found - self.assertTrue(url_found, 'OpenLP.io is not found') - - def test_ping_invalid(self): - """ - Test ping for OpenLP - """ - # GIVEN: a valid url to test - url = "trb143.io" - - # WHEN: Attempt to check the url exists - url_found = ping(url) - - # THEN: It should be found - self.assertFalse(url_found, 'TRB143.io is found') From 99b7239bc0a743c1194cf7b1a2443da276f26fa9 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 19 Sep 2017 21:36:59 -0700 Subject: [PATCH 15/52] A little cleanup --- openlp/plugins/bibles/lib/importers/http.py | 5 ----- openlp/plugins/remotes/deploy.py | 14 +++++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index c6dc90667..ffc2ddfbf 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -289,11 +289,6 @@ class BGExtract(RegistryProperties): if not page_source: send_error_message('download') return None - # TODO: Is this even necessary anymore? - # try: - # page_source = str(page_source, 'utf8') - # except UnicodeDecodeError: - # page_source = str(page_source, 'cp1251') try: soup = BeautifulSoup(page_source, 'lxml') except Exception: diff --git a/openlp/plugins/remotes/deploy.py b/openlp/plugins/remotes/deploy.py index d159c9b16..a56792f62 100644 --- a/openlp/plugins/remotes/deploy.py +++ b/openlp/plugins/remotes/deploy.py @@ -19,10 +19,11 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +""" +Download and "install" the remote web client +""" import os -import zipfile -import urllib.error +from zipfile import ZipFile from openlp.core.common import AppLocation, Registry from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size @@ -38,7 +39,7 @@ def deploy_zipfile(app_root, zip_name): :return: None """ zip_file = os.path.join(app_root, zip_name) - web_zip = zipfile.ZipFile(zip_file) + web_zip = ZipFile(zip_file) web_zip.extractall(app_root) @@ -48,9 +49,8 @@ def download_sha256(): """ user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() try: - web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'), - headers={'User-Agent': user_agent}) - except (urllib.error.URLError, ConnectionError) as err: + web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent}) + except ConnectionError: return False file_bits = web_config.split() return file_bits[0], file_bits[2] From c06cd39cab35e68ae0bfa5a865acfc08cc996baf Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 20 Sep 2017 09:55:21 -0700 Subject: [PATCH 16/52] Fix up all the tests --- openlp/core/common/httputils.py | 10 +- .../openlp_core_common/test_httputils.py | 208 ++++++++---------- .../openlp_core_ui/test_first_time.py | 26 +-- .../openlp_core_ui/test_firsttimeform.py | 25 +-- 4 files changed, 118 insertions(+), 151 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 0a5c962f8..d02626389 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -86,14 +86,13 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None): """ if not url: return None - if headers and 'user-agent' not in [key.lower() for key in headers.keys()]: + if not headers: + headers = {} + if 'user-agent' not in [key.lower() for key in headers.keys()]: headers['User-Agent'] = get_user_agent() log.debug('Downloading URL = %s' % url) retries = 0 while retries < CONNECTION_RETRIES: - # Put this at the bottom - # retries += 1 - # time.sleep(0.1) try: response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT)) log.debug('Downloaded page {url}'.format(url=response.url)) @@ -102,8 +101,9 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None): # For now, catch IOError. All requests errors inherit from IOError log.exception('Unable to connect to {url}'.format(url=url)) response = None - if retries > CONNECTION_RETRIES: + if retries >= CONNECTION_RETRIES: raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url)) + retries += 1 except: # Don't know what's happening, so reraise the original log.exception('Unknown error when trying to connect to {url}'.format(url=url)) diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 9cb32db25..a32f19172 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -24,7 +24,6 @@ Functional tests to test the AppLocation class and related methods. """ import os import tempfile -import socket from unittest import TestCase from unittest.mock import MagicMock, patch @@ -67,7 +66,7 @@ class TestHttpUtils(TestCase, TestMixin): """ with patch('openlp.core.common.httputils.sys') as mocked_sys: - # GIVEN: The system is Linux + # GIVEN: The system is Windows mocked_sys.platform = 'win32' # WHEN: We call get_user_agent() @@ -82,7 +81,7 @@ class TestHttpUtils(TestCase, TestMixin): """ with patch('openlp.core.common.httputils.sys') as mocked_sys: - # GIVEN: The system is Linux + # GIVEN: The system is macOS mocked_sys.platform = 'darwin' # WHEN: We call get_user_agent() @@ -97,7 +96,7 @@ class TestHttpUtils(TestCase, TestMixin): """ with patch('openlp.core.common.httputils.sys') as mocked_sys: - # GIVEN: The system is Linux + # GIVEN: The system is something else mocked_sys.platform = 'freebsd' # WHEN: We call get_user_agent() @@ -119,156 +118,127 @@ class TestHttpUtils(TestCase, TestMixin): # THEN: None should be returned self.assertIsNone(result, 'The return value of get_web_page should be None') - def test_get_web_page(self): + @patch('openlp.core.common.httputils.requests') + @patch('openlp.core.common.httputils.get_user_agent') + @patch('openlp.core.common.httputils.Registry') + def test_get_web_page(self, MockRegistry, mocked_get_user_agent, mocked_requests): """ Test that the get_web_page method works correctly """ - with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.common.Registry') as MockRegistry: - # GIVEN: Mocked out objects and a fake URL - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - fake_url = 'this://is.a.fake/url' + # GIVEN: Mocked out objects and a fake URL + mocked_requests.get.return_value = MagicMock(text='text') + mocked_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url) + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url) - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called') - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + # THEN: The correct methods are called with the correct arguments and a web page is returned + mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'}, + proxies=None, timeout=30.0) + mocked_get_user_agent.assert_called_once_with() + assert MockRegistry.call_count == 0, 'The Registry() object should have never been called' + assert returned_page == 'text', 'The returned page should be the mock object' - def test_get_web_page_with_header(self): + @patch('openlp.core.common.httputils.requests') + @patch('openlp.core.common.httputils.get_user_agent') + def test_get_web_page_with_header(self, mocked_get_user_agent, mocked_requests): """ Test that adding a header to the call to get_web_page() adds the header to the request """ - with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: - # GIVEN: Mocked out objects, a fake URL and a fake header - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - fake_url = 'this://is.a.fake/url' - fake_header = ('Fake-Header', 'fake value') + # GIVEN: Mocked out objects, a fake URL and a fake header + mocked_requests.get.return_value = MagicMock(text='text') + mocked_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + fake_headers = {'Fake-Header': 'fake value'} - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, header=fake_header) + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, headers=fake_headers) - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1]) - self.assertEqual(2, mocked_request_object.add_header.call_count, - 'There should only be 2 calls to add_header') - mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + # THEN: The correct methods are called with the correct arguments and a web page is returned + expected_headers = dict(fake_headers) + expected_headers.update({'User-Agent': 'user_agent'}) + mocked_requests.get.assert_called_once_with(fake_url, headers=expected_headers, + proxies=None, timeout=30.0) + mocked_get_user_agent.assert_called_with() + assert returned_page == 'text', 'The returned page should be the mock object' - def test_get_web_page_with_user_agent_in_headers(self): + @patch('openlp.core.common.httputils.requests') + @patch('openlp.core.common.httputils.get_user_agent') + def test_get_web_page_with_user_agent_in_headers(self, mocked_get_user_agent, mocked_requests): """ Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request """ - with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: - # GIVEN: Mocked out objects, a fake URL and a fake header - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - fake_url = 'this://is.a.fake/url' - user_agent_header = ('User-Agent', 'OpenLP/2.2.0') + # GIVEN: Mocked out objects, a fake URL and a fake header + mocked_requests.get.return_value = MagicMock(text='text') + fake_url = 'this://is.a.fake/url' + user_agent_headers = {'User-Agent': 'OpenLP/2.2.0'} - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, header=user_agent_header) + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, headers=user_agent_headers) - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1]) - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - self.assertEqual(0, mock_get_user_agent.call_count, 'get_user_agent should not have been called') - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + # THEN: The correct methods are called with the correct arguments and a web page is returned + mocked_requests.get.assert_called_once_with(fake_url, headers=user_agent_headers, + proxies=None, timeout=30.0) + assert mocked_get_user_agent.call_count == 0, 'get_user_agent() should not have been called' + assert returned_page == 'text', 'The returned page should be "test"' - def test_get_web_page_update_openlp(self): + @patch('openlp.core.common.httputils.requests') + @patch('openlp.core.common.httputils.get_user_agent') + @patch('openlp.core.common.httputils.Registry') + def test_get_web_page_update_openlp(self, MockRegistry, mocked_get_user_agent, mocked_requests): """ Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() """ - with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.common.httputils.Registry') as MockRegistry: - # GIVEN: Mocked out objects, a fake URL - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - mocked_registry_object = MagicMock() - mocked_application_object = MagicMock() - mocked_registry_object.get.return_value = mocked_application_object - MockRegistry.return_value = mocked_registry_object - fake_url = 'this://is.a.fake/url' + # GIVEN: Mocked out objects, a fake URL + mocked_requests.get.return_value = MagicMock(text='text') + mocked_get_user_agent.return_value = 'user_agent' + mocked_registry_object = MagicMock() + mocked_application_object = MagicMock() + mocked_registry_object.get.return_value = mocked_application_object + MockRegistry.return_value = mocked_registry_object + fake_url = 'this://is.a.fake/url' - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, update_openlp=True) + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, update_openlp=True) - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - mocked_registry_object.get.assert_called_with('application') - mocked_application_object.process_events.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + # THEN: The correct methods are called with the correct arguments and a web page is returned + mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'}, + proxies=None, timeout=30.0) + mocked_get_user_agent.assert_called_once_with() + mocked_registry_object.get.assert_called_with('application') + mocked_application_object.process_events.assert_called_with() + assert returned_page == 'text', 'The returned page should be the mock object' - def test_get_url_file_size(self): + @patch('openlp.core.common.httputils.requests') + def test_get_url_file_size(self, mocked_requests): """ - Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() + Test that calling "get_url_file_size" works correctly """ - with patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: - # GIVEN: Mocked out objects, a fake URL - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - fake_url = 'this://is.a.fake/url' + # GIVEN: Mocked out objects, a fake URL + mocked_requests.head.return_value = MagicMock(headers={'Content-Length': 100}) + fake_url = 'this://is.a.fake/url' - # WHEN: The get_url_file_size() method is called - get_url_file_size(fake_url) + # WHEN: The get_url_file_size() method is called + file_size = get_url_file_size(fake_url) - # THEN: The correct methods are called with the correct arguments and a web page is returned - mock_urlopen.assert_called_with(fake_url, timeout=30) + # THEN: The correct methods are called with the correct arguments and a web page is returned + mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, timeout=30.0) + assert file_size == 100 - @patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') - def test_socket_timeout(self, mocked_urlopen): + @patch('openlp.core.common.httputils.requests') + @patch('openlp.core.common.httputils.os.remove') + def test_socket_timeout(self, mocked_remove, mocked_requests): """ Test socket timeout gets caught """ # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download - mocked_urlopen.side_effect = socket.timeout() + mocked_requests.get.side_effect = IOError # WHEN: Attempt to retrieve a file - url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile) + url_get_file(MagicMock(), url='http://localhost/test', file_path=self.tempfile) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files - self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') + mocked_remove.assert_called_with(self.tempfile) + assert mocked_remove.call_count == 3, 'os.remove() should have been called 3 times' diff --git a/tests/functional/openlp_core_ui/test_first_time.py b/tests/functional/openlp_core_ui/test_first_time.py index 35507e15e..eb9464375 100644 --- a/tests/functional/openlp_core_ui/test_first_time.py +++ b/tests/functional/openlp_core_ui/test_first_time.py @@ -22,9 +22,6 @@ """ Package to test the openlp.core.utils.__init__ package. """ -import urllib.request -import urllib.error -import urllib.parse from unittest import TestCase from unittest.mock import patch @@ -37,20 +34,21 @@ class TestFirstTimeWizard(TestMixin, TestCase): """ Test First Time Wizard import functions """ - def test_webpage_connection_retry(self): + @patch('openlp.core.common.httputils.requests') + def test_webpage_connection_retry(self, mocked_requests): """ Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031 """ # GIVEN: Initial settings and mocks - with patch.object(urllib.request, 'urlopen') as mocked_urlopen: - mocked_urlopen.side_effect = ConnectionError + mocked_requests.get.side_effect = IOError('Unable to connect') - # WHEN: A webpage is requested - try: - get_web_page(url='http://localhost') - except: - pass + # WHEN: A webpage is requested + try: + get_web_page('http://localhost') + except Exception as e: + assert isinstance(e, ConnectionError) - # THEN: urlopen should have been called CONNECTION_RETRIES + 1 count - self.assertEquals(mocked_urlopen.call_count, CONNECTION_RETRIES + 1, - 'get_web_page() should have tried {} times'.format(CONNECTION_RETRIES)) + # THEN: urlopen should have been called CONNECTION_RETRIES + 1 count + assert mocked_requests.get.call_count == CONNECTION_RETRIES, \ + 'get should have been called {} times, but was only called {} times'.format( + CONNECTION_RETRIES, mocked_requests.get.call_count) diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 0f7c6be6a..c90cdd80b 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -34,7 +34,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm from tests.helpers.testmixin import TestMixin -FAKE_CONFIG = b""" +FAKE_CONFIG = """ [general] base url = http://example.com/frw/ [songs] @@ -45,7 +45,7 @@ directory = bibles directory = themes """ -FAKE_BROKEN_CONFIG = b""" +FAKE_BROKEN_CONFIG = """ [general] base url = http://example.com/frw/ [songs] @@ -54,7 +54,7 @@ directory = songs directory = bibles """ -FAKE_INVALID_CONFIG = b""" +FAKE_INVALID_CONFIG = """ This is not a config file Some text @@ -112,7 +112,7 @@ class TestFirstTimeForm(TestCase, TestMixin): patch('openlp.core.ui.firsttimeform.Settings') as MockedSettings, \ patch('openlp.core.ui.firsttimeform.gettempdir') as mocked_gettempdir, \ patch('openlp.core.ui.firsttimeform.check_directory_exists') as mocked_check_directory_exists, \ - patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: + patch.object(frw.application, 'set_normal_cursor'): mocked_settings = MagicMock() mocked_settings.value.return_value = True MockedSettings.return_value = mocked_settings @@ -192,7 +192,7 @@ class TestFirstTimeForm(TestCase, TestMixin): with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) - mocked_get_web_page.return_value.read.return_value = FAKE_BROKEN_CONFIG + mocked_get_web_page.return_value = FAKE_BROKEN_CONFIG # WHEN: The First Time Wizard is downloads the config file first_time_form._download_index() @@ -208,7 +208,7 @@ class TestFirstTimeForm(TestCase, TestMixin): with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) - mocked_get_web_page.return_value.read.return_value = FAKE_INVALID_CONFIG + mocked_get_web_page.return_value = FAKE_INVALID_CONFIG # WHEN: The First Time Wizard is downloads the config file first_time_form._download_index() @@ -225,14 +225,13 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: Initial setup and mocks first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) - mocked_get_web_page.side_effect = urllib.error.HTTPError(url='http//localhost', - code=407, - msg='Network proxy error', - hdrs=None, - fp=None) + mocked_get_web_page.side_effect = ConnectionError('') + mocked_message_box.Ok = 'OK' + # WHEN: the First Time Wizard calls to get the initial configuration first_time_form._download_index() # THEN: the critical_error_message_box should have been called - self.assertEquals(mocked_message_box.mock_calls[1][1][0], 'Network Error 407', - 'first_time_form should have caught Network Error') + mocked_message_box.critical.assert_called_once_with( + first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve ' + 'initial configuration information', 'OK') From b440584cb54a68200bd1d5bd53eea313c9a5b834 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 20 Sep 2017 21:44:57 +0100 Subject: [PATCH 17/52] Moved the patched shuilils to the path module --- openlp/core/__init__.py | 3 +- openlp/core/common/path.py | 117 ++++++++++ openlp/core/lib/__init__.py | 29 --- openlp/core/lib/shutil.py | 112 ---------- openlp/core/ui/lib/filedialog.py | 3 +- openlp/core/ui/mainwindow.py | 3 +- .../presentations/lib/pdfcontroller.py | 3 +- .../lib/presentationcontroller.py | 3 +- .../openlp_core_common/test_path.py | 203 +++++++++++++++++- tests/functional/openlp_core_lib/test_lib.py | 34 +-- .../functional/openlp_core_lib/test_shutil.py | 170 --------------- 11 files changed, 325 insertions(+), 355 deletions(-) delete mode 100755 openlp/core/lib/shutil.py delete mode 100755 tests/functional/openlp_core_lib/test_shutil.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 0fcea2d1a..8cd62a97f 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -38,10 +38,9 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, is_macosx, is_win, translate -from openlp.core.common.path import Path +from openlp.core.common.path import Path, copytree from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList -from openlp.core.lib.shutil import copytree from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen from openlp.core.ui.exceptionform import ExceptionForm diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index 3c4dd93c9..f11c4bb9f 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -19,6 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +import shutil from contextlib import suppress from openlp.core.common import is_win @@ -29,6 +30,121 @@ else: from pathlib import PosixPath as PathVariant +def replace_params(args, kwargs, params): + """ + Apply a transformation function to the specified args or kwargs + + :param tuple args: Positional arguments + :param dict kwargs: Key Word arguments + :param params: A tuple of tuples with the position and the key word to replace. + :return: The modified positional and keyword arguments + :rtype: tuple[tuple, dict] + + + Usage: + Take a method with the following signature, and assume we which to apply the str function to arg2: + def method(arg1=None, arg2=None, arg3=None) + + As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we + would call this function as follows: + + replace_params(args, kwargs, ((1, 'arg2', str),)) + """ + args = list(args) + for position, key_word, transform in params: + if len(args) > position: + args[position] = transform(args[position]) + elif key_word in kwargs: + kwargs[key_word] = transform(kwargs[key_word]) + return tuple(args), kwargs + + +def copy(*args, **kwargs): + """ + Wraps :func:`shutil.copy` so that we can accept Path objects. + + :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copy + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copy(*args, **kwargs)) + + +def copyfile(*args, **kwargs): + """ + Wraps :func:`shutil.copyfile` so that we can accept Path objects. + + :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copyfile + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copyfile(*args, **kwargs)) + + +def copytree(*args, **kwargs): + """ + Wraps :func:shutil.copytree` so that we can accept Path objects. + + :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copytree(*args, **kwargs)) + + +def rmtree(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) + + return shutil.rmtree(*args, **kwargs) + + +def which(*args, **kwargs): + """ + Wraps :func:shutil.which` so that it return a Path objects. + + :rtype: openlp.core.common.Path + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.which + """ + file_name = shutil.which(*args, **kwargs) + if file_name: + return str_to_path(file_name) + return None + + def path_to_str(path=None): """ A utility function to convert a Path object or NoneType to a string equivalent. @@ -98,3 +214,4 @@ class Path(PathVariant): with suppress(ValueError): path = path.relative_to(base_path) return {'__Path__': path.parts} + diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 1d55df497..4602fee2c 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -611,35 +611,6 @@ def create_separated_list(string_list): return list_to_string -def replace_params(args, kwargs, params): - """ - Apply a transformation function to the specified args or kwargs - - :param tuple args: Positional arguments - :param dict kwargs: Key Word arguments - :param params: A tuple of tuples with the position and the key word to replace. - :return: The modified positional and keyword arguments - :rtype: tuple[tuple, dict] - - - Usage: - Take a method with the following signature, and assume we which to apply the str function to arg2: - def method(arg1=None, arg2=None, arg3=None) - - As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we - would call this function as follows: - - replace_params(args, kwargs, ((1, 'arg2', str),)) - """ - args = list(args) - for position, key_word, transform in params: - if len(args) > position: - args[position] = transform(args[position]) - elif key_word in kwargs: - kwargs[key_word] = transform(kwargs[key_word]) - return tuple(args), kwargs - - from .exceptions import ValidationError from .screen import ScreenList from .formattingtags import FormattingTags diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py deleted file mode 100755 index 1c7a9a393..000000000 --- a/openlp/core/lib/shutil.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- 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 shutil methods we use so they accept and return Path objects""" -import shutil - -from openlp.core.common.path import path_to_str, str_to_path -from openlp.core.lib import replace_params - - -def copy(*args, **kwargs): - """ - Wraps :func:`shutil.copy` so that we can accept Path objects. - - :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object - :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copy - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copy(*args, **kwargs)) - - -def copyfile(*args, **kwargs): - """ - Wraps :func:`shutil.copyfile` so that we can accept Path objects. - - :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object - :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copyfile - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copyfile(*args, **kwargs)) - - -def copytree(*args, **kwargs): - """ - Wraps :func:shutil.copytree` so that we can accept Path objects. - - :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object - :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copytree - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copytree(*args, **kwargs)) - - -def rmtree(*args, **kwargs): - """ - Wraps :func:shutil.rmtree` so that we can accept Path objects. - - :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object - :return: Passes the return from :func:`shutil.rmtree` back - :rtype: None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.rmtree - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) - - return shutil.rmtree(*args, **kwargs) - - -def which(*args, **kwargs): - """ - Wraps :func:shutil.which` so that it return a Path objects. - - :rtype: openlp.core.common.Path - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.which - """ - file_name = shutil.which(*args, **kwargs) - if file_name: - return str_to_path(file_name) - return None diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/ui/lib/filedialog.py index d4a702e83..0f3ef4058 100755 --- a/openlp/core/ui/lib/filedialog.py +++ b/openlp/core/ui/lib/filedialog.py @@ -22,8 +22,7 @@ """ Patch the QFileDialog so it accepts and returns Path objects""" from PyQt5 import QtWidgets -from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.lib import replace_params +from openlp.core.common.path import Path, path_to_str, replace_params, str_to_path class FileDialog(QtWidgets.QFileDialog): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 88799b060..4b505b807 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -39,10 +39,9 @@ from openlp.core.api.http import server from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder -from openlp.core.common.path import Path, path_to_str, str_to_path +from openlp.core.common.path import Path, copyfile, path_to_str, str_to_path from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon -from openlp.core.lib.shutil import copyfile from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index a39cce36c..81fa3994a 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -27,9 +27,8 @@ from subprocess import check_output, CalledProcessError from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import Settings, is_win -from openlp.core.common.path import Path, path_to_str +from openlp.core.common.path import which from openlp.core.lib import ScreenList -from openlp.core.lib.shutil import which from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument if is_win(): diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 3225eac24..304d70833 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -24,9 +24,8 @@ import logging from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash -from openlp.core.common.path import Path +from openlp.core.common.path import Path, rmtree from openlp.core.lib import create_thumb, validate_thumb -from openlp.core.lib.shutil import rmtree log = logging.getLogger(__name__) diff --git a/tests/functional/openlp_core_common/test_path.py b/tests/functional/openlp_core_common/test_path.py index f5abcffd5..0f35319c9 100644 --- a/tests/functional/openlp_core_common/test_path.py +++ b/tests/functional/openlp_core_common/test_path.py @@ -24,8 +24,209 @@ Package to test the openlp.core.common.path package. """ import os from unittest import TestCase +from unittest.mock import ANY, MagicMock, patch -from openlp.core.common.path import Path, path_to_str, str_to_path +from openlp.core.common.path import Path, copy, copyfile, copytree, path_to_str, replace_params, rmtree, str_to_path, \ + which + + +class TestShutil(TestCase): + """ + Tests for the :mod:`openlp.core.common.path` module + """ + def test_replace_params_no_params(self): + """ + Test replace_params when called with and empty tuple instead of parameters to replace + """ + # GIVEN: Some test data + test_args = (1, 2) + test_kwargs = {'arg3': 3, 'arg4': 4} + test_params = tuple() + + # WHEN: Calling replace_params + result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) + + # THEN: The positional and keyword args should not have changed + self.assertEqual(test_args, result_args) + self.assertEqual(test_kwargs, result_kwargs) + + def test_replace_params_params(self): + """ + Test replace_params when given a positional and a keyword argument to change + """ + # GIVEN: Some test data + test_args = (1, 2) + test_kwargs = {'arg3': 3, 'arg4': 4} + test_params = ((1, 'arg2', str), (2, 'arg3', str)) + + # WHEN: Calling replace_params + result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) + + # THEN: The positional and keyword args should have have changed + self.assertEqual(result_args, (1, '2')) + self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4}) + + def test_copy(self): + """ + Test :func:`openlp.core.common.path.copy` + """ + # GIVEN: A mocked `shutil.copy` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copy: + + # WHEN: Calling :func:`openlp.core.common.path.copy` with the src and dst parameters as Path object types + result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copy` should return the str type result of calling + # :func:`shutil.copy` as a Path object. + mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copy_follow_optional_params(self): + """ + Test :func:`openlp.core.common.path.copy` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copy` + with patch('openlp.core.common.path.shutil.copy', return_value='') as mocked_shutil_copy: + + # WHEN: Calling :func:`openlp.core.common.path.copy` with :param:`follow_symlinks` set to False + copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) + + # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false + mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_copyfile(self): + """ + Test :func:`openlp.core.common.path.copyfile` + """ + # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copyfile', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: + + # WHEN: Calling :func:`openlp.core.common.path.copyfile` with the src and dst parameters as Path object + # types + result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copyfile` should return the str type result of calling + # :func:`shutil.copyfile` as a Path object. + mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copyfile_optional_params(self): + """ + Test :func:`openlp.core.common.path.copyfile` when follow_symlinks is set to false + """ + # GIVEN: A mocked :func:`shutil.copyfile` + with patch('openlp.core.common.path.shutil.copyfile', return_value='') as mocked_shutil_copyfile: + + # WHEN: Calling :func:`openlp.core.common.path.copyfile` with :param:`follow_symlinks` set to False + copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) + + # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_copytree(self): + """ + Test :func:`openlp.core.common.path.copytree` + """ + # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copytree', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: + + # WHEN: Calling :func:`openlp.core.common.path.copytree` with the src and dst parameters as Path object + # types + result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copytree` should return the str type result of calling + # :func:`shutil.copytree` as a Path object. + mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copytree_optional_params(self): + """ + Test :func:`openlp.core.common.path.copytree` when optional parameters are passed + """ + # GIVEN: A mocked :func:`shutil.copytree` + with patch('openlp.core.common.path.shutil.copytree', return_value='') as mocked_shutil_copytree: + mocked_ignore = MagicMock() + mocked_copy_function = MagicMock() + + # WHEN: Calling :func:`openlp.core.common.path.copytree` with the optional parameters set + copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, + ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) + + # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + def test_rmtree(self): + """ + Test :func:`rmtree` + """ + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: + + # WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type + result = rmtree(Path('test', 'path')) + + # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) + self.assertIsNone(result) + + def test_rmtree_optional_params(self): + """ + Test :func:`openlp.core.common.path.rmtree` when optional parameters are passed + """ + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.common.path.shutil.rmtree', return_value='') as mocked_shutil_rmtree: + mocked_on_error = MagicMock() + + # WHEN: Calling :func:`openlp.core.common.path.rmtree` with :param:`ignore_errors` set to True and + # :param:`onerror` set to a mocked object + rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + + # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) + + def test_which_no_command(self): + """ + Test :func:`openlp.core.common.path.which` when the command is not found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is not found. + with patch('openlp.core.common.path.shutil.which', return_value=None) as mocked_shutil_which: + + # WHEN: Calling :func:`openlp.core.common.path.which` with a command that does not exist. + result = which('no_command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. + mocked_shutil_which.assert_called_once_with('no_command') + self.assertIsNone(result) + + def test_which_command(self): + """ + Test :func:`openlp.core.common.path.which` when a command has been found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is found. + with patch('openlp.core.common.path.shutil.which', + return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: + + # WHEN: Calling :func:`openlp.core.common.path.which` with a command that exists. + result = which('command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a + # Path object equivalent of the command path. + mocked_shutil_which.assert_called_once_with('command') + self.assertEqual(result, Path('path', 'to', 'command')) class TestPath(TestCase): diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 8b46e99c3..96e78e351 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -32,7 +32,7 @@ from PyQt5 import QtCore, QtGui from openlp.core.common.path import Path from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \ create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \ - get_text_file_string, image_to_byte, replace_params, resize_image, str_to_bool, validate_thumb + get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) @@ -636,38 +636,6 @@ class TestLib(TestCase): thumb_path.stat.assert_called_once_with() self.assertFalse(result, 'The result should be False') - def test_replace_params_no_params(self): - """ - Test replace_params when called with and empty tuple instead of parameters to replace - """ - # GIVEN: Some test data - test_args = (1, 2) - test_kwargs = {'arg3': 3, 'arg4': 4} - test_params = tuple() - - # WHEN: Calling replace_params - result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) - - # THEN: The positional and keyword args should not have changed - self.assertEqual(test_args, result_args) - self.assertEqual(test_kwargs, result_kwargs) - - def test_replace_params_params(self): - """ - Test replace_params when given a positional and a keyword argument to change - """ - # GIVEN: Some test data - test_args = (1, 2) - test_kwargs = {'arg3': 3, 'arg4': 4} - test_params = ((1, 'arg2', str), (2, 'arg3', str)) - - # WHEN: Calling replace_params - result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) - - # THEN: The positional and keyword args should have have changed - self.assertEqual(result_args, (1, '2')) - self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4}) - def test_resize_thumb(self): """ Test the resize_thumb() function diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py deleted file mode 100755 index 737f7ce00..000000000 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ /dev/null @@ -1,170 +0,0 @@ -import os -from unittest import TestCase -from unittest.mock import ANY, MagicMock, patch - -from openlp.core.common.path import Path -from openlp.core.lib.shutil import copy, copyfile, copytree, rmtree, which - - -class TestShutil(TestCase): - """ - Tests for the :mod:`openlp.core.lib.shutil` module - """ - - def test_copy(self): - """ - Test :func:`copy` - """ - # GIVEN: A mocked `shutil.copy` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copy: - - # WHEN: Calling :func:`copy` with the src and dst parameters as Path object types - result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. - # :func:`copy` should return the str type result of calling :func:`shutil.copy` as a Path object. - mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copy_follow_optional_params(self): - """ - Test :func:`copy` when follow_symlinks is set to false - """ - # GIVEN: A mocked `shutil.copy` - with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: - - # WHEN: Calling :func:`copy` with :param:`follow_symlinks` set to False - copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - - # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false - mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) - - def test_copyfile(self): - """ - Test :func:`copyfile` - """ - # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copyfile', - return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: - - # WHEN: Calling :func:`copyfile` with the src and dst parameters as Path object types - result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. - # :func:`copyfile` should return the str type result of calling :func:`shutil.copyfile` as a Path - # object. - mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copyfile_optional_params(self): - """ - Test :func:`copyfile` when follow_symlinks is set to false - """ - # GIVEN: A mocked :func:`shutil.copyfile` - with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: - - # WHEN: Calling :func:`copyfile` with :param:`follow_symlinks` set to False - copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - - # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) - - def test_copytree(self): - """ - Test :func:`copytree` - """ - # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copytree', - return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: - - # WHEN: Calling :func:`copytree` with the src and dst parameters as Path object types - result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. - # :func:`patches.copytree` should return the str type result of calling :func:`shutil.copytree` as a - # Path object. - mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copytree_optional_params(self): - """ - Test :func:`copytree` when optional parameters are passed - """ - # GIVEN: A mocked :func:`shutil.copytree` - with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: - mocked_ignore = MagicMock() - mocked_copy_function = MagicMock() - - # WHEN: Calling :func:`copytree` with the optional parameters set - copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, - ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - - # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, - copy_function=mocked_copy_function, - ignore_dangling_symlinks=True) - - def test_rmtree(self): - """ - Test :func:`rmtree` - """ - # GIVEN: A mocked :func:`shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: - - # WHEN: Calling :func:`rmtree` with the path parameter as Path object type - result = rmtree(Path('test', 'path')) - - # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. - mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) - self.assertIsNone(result) - - def test_rmtree_optional_params(self): - """ - Test :func:`rmtree` when optional parameters are passed - """ - # GIVEN: A mocked :func:`shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: - mocked_on_error = MagicMock() - - # WHEN: Calling :func:`rmtree` with :param:`ignore_errors` set to True and `onerror` set to a mocked object - rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) - - # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) - - def test_which_no_command(self): - """ - Test :func:`which` when the command is not found. - """ - # GIVEN: A mocked :func:``shutil.which` when the command is not found. - with patch('openlp.core.lib.shutil.shutil.which', return_value=None) as mocked_shutil_which: - - # WHEN: Calling :func:`which` with a command that does not exist. - result = which('no_command') - - # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. - mocked_shutil_which.assert_called_once_with('no_command') - self.assertIsNone(result) - - def test_which_command(self): - """ - Test :func:`which` when a command has been found. - """ - # GIVEN: A mocked :func:`shutil.which` when the command is found. - with patch('openlp.core.lib.shutil.shutil.which', - return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: - - # WHEN: Calling :func:`which` with a command that exists. - result = which('command') - - # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a - # Path object equivalent of the command path. - mocked_shutil_which.assert_called_once_with('command') - self.assertEqual(result, Path('path', 'to', 'command')) From b4a687c85e102088a7f77ad1536942561627fa81 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 21 Sep 2017 07:20:56 +0100 Subject: [PATCH 18/52] PEP fixes --- openlp/core/common/path.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index f11c4bb9f..cdb115940 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -214,4 +214,3 @@ class Path(PathVariant): with suppress(ValueError): path = path.relative_to(base_path) return {'__Path__': path.parts} - From 131e0213e2c93b938a539491e18fca2bf76b1866 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 21 Sep 2017 08:40:41 +0100 Subject: [PATCH 19/52] minor fix --- openlp/plugins/presentations/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index d9a14e0ed..8061bb193 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -331,7 +331,7 @@ class PresentationMediaItem(MediaManagerItem): file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) path, file_name = file_path.parent, file_path.name service_item.title = file_name - if file_path.exists: + if file_path.exists(): if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic': service_item.processor = self.find_controller_by_type(file_path) if not service_item.processor: From e1ca15173d13b0f82d5e940134e316bde7b3d852 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 22 Sep 2017 20:50:45 -0700 Subject: [PATCH 20/52] Actually show the progress dialog when we're waiting for the thread to shut down. --- openlp.py | 3 ++- openlp/core/ui/mainwindow.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openlp.py b/openlp.py index 68001f2d1..73c0d9033 100755 --- a/openlp.py +++ b/openlp.py @@ -28,9 +28,10 @@ import multiprocessing import sys from openlp.core.common import is_win, is_macosx +from openlp.core.common.applocation import AppLocation from openlp.core import main -faulthandler.enable() +faulthandler.enable(open(str(AppLocation.get_directory(AppLocation.CacheDir) / 'error.log'), 'wb')) if __name__ == '__main__': """ diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index ae32c0347..14b34a715 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1017,7 +1017,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self) wait_dialog.setWindowModality(QtCore.Qt.WindowModal) wait_dialog.setAutoClose(False) - self.version_thread.wait() + wait_dialog.show() + retry = 0 + while self.version_thread.isRunning() and retry < 10: + self.application.processEvents() + self.version_thread.wait(500) + if self.version_thread.isRunning(): + self.version_thread.terminate() wait_dialog.close() except RuntimeError: # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object From a4bd979f19a9c9e8d82d6b9f978f67c2aed35417 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 22 Sep 2017 21:10:18 -0700 Subject: [PATCH 21/52] Hide the cancel button --- openlp/core/ui/mainwindow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 4424944cf..508b6a34c 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1017,11 +1017,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self) wait_dialog.setWindowModality(QtCore.Qt.WindowModal) wait_dialog.setAutoClose(False) + wait_dialog.setCancelButton(None) wait_dialog.show() retry = 0 - while self.version_thread.isRunning() and retry < 10: + while self.version_thread.isRunning() and retry < 50: self.application.processEvents() - self.version_thread.wait(500) + self.version_thread.wait(100) + retry += 1 if self.version_thread.isRunning(): self.version_thread.terminate() wait_dialog.close() From 7239b1e400e947f180c912b528ec0c21804516cc Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 22 Sep 2017 22:23:56 -0700 Subject: [PATCH 22/52] Add a mock main_window --- tests/interfaces/openlp_plugins/bibles/test_lib_http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index e4075ca19..859957f8d 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -38,6 +38,7 @@ class TestBibleHTTP(TestCase): Registry.create() Registry().register('service_list', MagicMock()) Registry().register('application', MagicMock()) + Registry().register('main_window', MagicMock()) def test_bible_gateway_extract_books(self): """ From d61ed7e9b1d64c1000e3ab73e3b7fefee967d1c6 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sat, 23 Sep 2017 14:06:42 +0100 Subject: [PATCH 23/52] converted the image plugin over to using Path objects --- openlp/core/lib/db.py | 80 +++++++++-- openlp/core/ui/servicemanager.py | 8 +- openlp/plugins/images/imageplugin.py | 4 +- openlp/plugins/images/lib/db.py | 7 +- openlp/plugins/images/lib/imagetab.py | 3 - openlp/plugins/images/lib/mediaitem.py | 134 +++++++++--------- openlp/plugins/images/lib/upgrade.py | 70 +++++++++ .../openlp_plugins/images/test_lib.py | 40 +++--- .../openlp_plugins/images/test_upgrade.py | 83 +++++++++++ tests/resources/images/image-v0.sqlite | Bin 0 -> 12288 bytes 10 files changed, 317 insertions(+), 112 deletions(-) create mode 100644 openlp/plugins/images/lib/upgrade.py create mode 100644 tests/functional/openlp_plugins/images/test_upgrade.py create mode 100644 tests/resources/images/image-v0.sqlite diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 6d13c1d9b..9c594b14e 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -23,12 +23,13 @@ """ The :mod:`db` module provides the core database functionality for OpenLP """ +import json import logging import os from copy import copy from urllib.parse import quote_plus as urlquote -from sqlalchemy import Table, MetaData, Column, types, create_engine +from sqlalchemy import Table, MetaData, Column, types, create_engine, UnicodeText from sqlalchemy.engine.url import make_url from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError from sqlalchemy.orm import scoped_session, sessionmaker, mapper @@ -37,7 +38,8 @@ from sqlalchemy.pool import NullPool from alembic.migration import MigrationContext from alembic.operations import Operations -from openlp.core.common import AppLocation, Settings, translate, delete_file +from openlp.core.common import AppLocation, Settings, delete_file, translate +from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder from openlp.core.lib.ui import critical_error_message_box log = logging.getLogger(__name__) @@ -133,9 +135,10 @@ def get_db_path(plugin_name, db_file_name=None): if db_file_name is None: return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name), plugin=plugin_name) + elif os.path.isabs(db_file_name): + return 'sqlite:///{db_file_name}'.format(db_file_name=db_file_name) else: - return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), - name=db_file_name) + return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), name=db_file_name) def handle_db_error(plugin_name, db_file_name): @@ -200,6 +203,54 @@ class BaseModel(object): return instance +class PathType(types.TypeDecorator): + """ + Create a PathType for storing Path objects with SQLAlchemy. Behind the scenes we convert the Path object to a JSON + representation and store it as a Unicode type + """ + impl = types.UnicodeText + + def coerce_compared_value(self, op, value): + """ + Some times it make sense to compare a PathType with a string. In the case a string is used coerce the the + PathType to a UnicodeText type. + + :param op: The operation being carried out. Not used, as we only care about the type that is being used with the + operation. + :param openlp.core.common.path.Path | str value: The value being used for the comparison. Most likely a Path Object or str. + :return: The coerced value stored in the db + :rtype: PathType or UnicodeText + """ + if isinstance(value, str): + return UnicodeText() + else: + return self + + def process_bind_param(self, value, dialect): + """ + Convert the Path object to a JSON representation + + :param openlp.core.common.path.Path value: The value to convert + :param dialect: Not used + :return: The Path object as a JSON string + :rtype: str + """ + data_path = AppLocation.get_data_path() + return json.dumps(value, cls=OpenLPJsonEncoder, base_path=data_path) + + def process_result_value(self, value, dialect): + """ + Convert the JSON representation back + + :param types.UnicodeText value: The value to convert + :param dialect: Not used + :return: The JSON object converted Python object (in this case it should be a Path object) + :rtype: openlp.core.common.path.Path + """ + data_path = AppLocation.get_data_path() + return json.loads(value, cls=OpenLPJsonDecoder, base_path=data_path) + + def upgrade_db(url, upgrade): """ Upgrade a database. @@ -208,7 +259,7 @@ def upgrade_db(url, upgrade): :param upgrade: The python module that contains the upgrade instructions. """ if not database_exists(url): - log.warn("Database {db} doesn't exist - skipping upgrade checks".format(db=url)) + log.warning("Database {db} doesn't exist - skipping upgrade checks".format(db=url)) return (0, 0) log.debug('Checking upgrades for DB {db}'.format(db=url)) @@ -273,10 +324,11 @@ def delete_database(plugin_name, db_file_name=None): :param plugin_name: The name of the plugin to remove the database for :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used. """ + db_file_path = AppLocation.get_section_data_path(plugin_name) if db_file_name: - db_file_path = AppLocation.get_section_data_path(plugin_name) / db_file_name + db_file_path = db_file_path / db_file_name else: - db_file_path = AppLocation.get_section_data_path(plugin_name) / plugin_name + db_file_path = db_file_path / plugin_name return delete_file(db_file_path) @@ -284,30 +336,30 @@ class Manager(object): """ Provide generic object persistence management """ - def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None, session=None): + def __init__(self, plugin_name, init_schema, db_file_path=None, upgrade_mod=None, session=None): """ Runs the initialisation process that includes creating the connection to the database and the tables if they do not exist. :param plugin_name: The name to setup paths and settings section names :param init_schema: The init_schema function for this database - :param db_file_name: The upgrade_schema function for this database - :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name + :param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None resulting in the plugin_name being used. + :param upgrade_mod: The upgrade_schema function for this database """ self.is_dirty = False self.session = None self.db_url = None - if db_file_name: + if db_file_path: log.debug('Manager: Creating new DB url') - self.db_url = init_url(plugin_name, db_file_name) + self.db_url = init_url(plugin_name, str(db_file_path)) else: self.db_url = init_url(plugin_name) if upgrade_mod: try: db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) except (SQLAlchemyError, DBAPIError): - handle_db_error(plugin_name, db_file_name) + handle_db_error(plugin_name, str(db_file_path)) return if db_ver > up_ver: critical_error_message_box( @@ -322,7 +374,7 @@ class Manager(object): try: self.session = init_schema(self.db_url) except (SQLAlchemyError, DBAPIError): - handle_db_error(plugin_name, db_file_name) + handle_db_error(plugin_name, str(db_file_path)) else: self.session = session diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index b393ad736..b5b77d265 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -376,7 +376,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self._file_name = path_to_str(file_path) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) Settings().setValue('servicemanager/last file', file_path) - if file_path and file_path.suffix() == '.oszl': + if file_path and file_path.suffix == '.oszl': self._save_lite = True else: self._save_lite = False @@ -699,13 +699,15 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa default_file_name = format_time(default_pattern, local_time) else: default_file_name = '' + default_file_path = Path(default_file_name) directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory') - file_path = directory_path / default_file_name + if directory_path: + default_file_path = directory_path / default_file_path # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # the long term. if self._file_name.endswith('oszl') or self.service_has_all_original_files: file_path, filter_used = FileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, file_path, + self.main_window, UiStrings().SaveService, default_file_path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) else: diff --git a/openlp/plugins/images/imageplugin.py b/openlp/plugins/images/imageplugin.py index a4310f170..36051a505 100644 --- a/openlp/plugins/images/imageplugin.py +++ b/openlp/plugins/images/imageplugin.py @@ -29,7 +29,7 @@ from openlp.core.common import Settings, translate from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon from openlp.core.lib.db import Manager from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint -from openlp.plugins.images.lib import ImageMediaItem, ImageTab +from openlp.plugins.images.lib import ImageMediaItem, ImageTab, upgrade from openlp.plugins.images.lib.db import init_schema log = logging.getLogger(__name__) @@ -50,7 +50,7 @@ class ImagePlugin(Plugin): def __init__(self): super(ImagePlugin, self).__init__('images', __default_settings__, ImageMediaItem, ImageTab) - self.manager = Manager('images', init_schema) + self.manager = Manager('images', init_schema, upgrade_mod=upgrade) self.weight = -7 self.icon_path = ':/plugins/plugin_images.png' self.icon = build_icon(self.icon_path) diff --git a/openlp/plugins/images/lib/db.py b/openlp/plugins/images/lib/db.py index feb1174a8..be7c299b3 100644 --- a/openlp/plugins/images/lib/db.py +++ b/openlp/plugins/images/lib/db.py @@ -22,11 +22,10 @@ """ The :mod:`db` module provides the database and schema that is the backend for the Images plugin. """ - from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy.orm import mapper -from openlp.core.lib.db import BaseModel, init_db +from openlp.core.lib.db import BaseModel, PathType, init_db class ImageGroups(BaseModel): @@ -65,7 +64,7 @@ def init_schema(url): * id * group_id - * filename + * file_path """ session, metadata = init_db(url) @@ -80,7 +79,7 @@ def init_schema(url): image_filenames_table = Table('image_filenames', metadata, Column('id', types.Integer(), primary_key=True), Column('group_id', types.Integer(), ForeignKey('image_groups.id'), default=None), - Column('filename', types.Unicode(255), nullable=False) + Column('file_path', PathType(), nullable=False) ) mapper(ImageGroups, image_groups_table) diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index bacf03ce6..565ef6543 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -31,9 +31,6 @@ class ImageTab(SettingsTab): """ ImageTab is the images settings tab in the settings dialog. """ - def __init__(self, parent, name, visible_title, icon_path): - super(ImageTab, self).__init__(parent, name, visible_title, icon_path) - def setupUi(self): self.setObjectName('ImagesTab') super(ImageTab, self).setupUi() diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index d1ea2003f..846c731fe 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -21,7 +21,6 @@ ############################################################################### import logging -import os from PyQt5 import QtCore, QtGui, QtWidgets @@ -49,7 +48,7 @@ class ImageMediaItem(MediaManagerItem): log.info('Image Media Item loaded') def __init__(self, parent, plugin): - self.icon_path = 'images/image' + self.icon_resource = 'images/image' self.manager = None self.choose_group_form = None self.add_group_form = None @@ -99,11 +98,11 @@ class ImageMediaItem(MediaManagerItem): self.list_view.setIconSize(QtCore.QSize(88, 50)) self.list_view.setIndentation(self.list_view.default_indentation) self.list_view.allow_internal_dnd = True - self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') - check_directory_exists(Path(self.service_path)) + self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails' + check_directory_exists(self.service_path) # Load images from the database self.load_full_list( - self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True) + self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path), initial_load=True) def add_list_view_to_toolbar(self): """ @@ -211,8 +210,8 @@ class ImageMediaItem(MediaManagerItem): """ images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id) for image in images: - delete_file(Path(self.service_path, os.path.split(image.filename)[1])) - delete_file(Path(self.generate_thumbnail_path(image))) + delete_file(self.service_path / image.file_path.name) + delete_file(self.generate_thumbnail_path(image)) self.manager.delete_object(ImageFilenames, image.id) image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id) for group in image_groups: @@ -234,8 +233,8 @@ class ImageMediaItem(MediaManagerItem): if row_item: item_data = row_item.data(0, QtCore.Qt.UserRole) if isinstance(item_data, ImageFilenames): - delete_file(Path(self.service_path, row_item.text(0))) - delete_file(Path(self.generate_thumbnail_path(item_data))) + delete_file(self.service_path / row_item.text(0)) + delete_file(self.generate_thumbnail_path(item_data)) if item_data.group_id == 0: self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item)) else: @@ -326,17 +325,19 @@ class ImageMediaItem(MediaManagerItem): """ Generate a path to the thumbnail - :param image: An instance of ImageFileNames - :return: A path to the thumbnail of type str + :param openlp.plugins.images.lib.db.ImageFilenames image: The image to generate the thumbnail path for. + :return: A path to the thumbnail + :rtype: openlp.core.common.path.Path """ - ext = os.path.splitext(image.filename)[1].lower() - return os.path.join(self.service_path, '{}{}'.format(str(image.id), ext)) + ext = image.file_path.suffix.lower() + return self.service_path / '{name:d}{ext}'.format(name=image.id, ext=ext) def load_full_list(self, images, initial_load=False, open_group=None): """ Replace the list of images and groups in the interface. - :param images: A List of Image Filenames objects that will be used to reload the mediamanager list. + :param list[openlp.plugins.images.lib.db.ImageFilenames] images: A List of Image Filenames objects that will be + used to reload the mediamanager list. :param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images. :param open_group: ImageGroups object of the group that must be expanded after reloading the list in the interface. @@ -352,34 +353,34 @@ class ImageMediaItem(MediaManagerItem): self.expand_group(open_group.id) # Sort the images by its filename considering language specific. # characters. - images.sort(key=lambda image_object: get_locale_key(os.path.split(str(image_object.filename))[1])) - for image_file in images: - log.debug('Loading image: {name}'.format(name=image_file.filename)) - filename = os.path.split(image_file.filename)[1] - thumb = self.generate_thumbnail_path(image_file) - if not os.path.exists(image_file.filename): + images.sort(key=lambda image_object: get_locale_key(image_object.file_path.name)) + for image in images: + log.debug('Loading image: {name}'.format(name=image.file_path)) + file_name = image.file_path.name + thumbnail_path = self.generate_thumbnail_path(image) + if not image.file_path.exists(): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(Path(image_file.filename), Path(thumb)): - icon = build_icon(thumb) + if validate_thumb(image.file_path, thumbnail_path): + icon = build_icon(thumbnail_path) else: - icon = create_thumb(image_file.filename, thumb) - item_name = QtWidgets.QTreeWidgetItem([filename]) - item_name.setText(0, filename) + icon = create_thumb(image.file_path, thumbnail_path) + item_name = QtWidgets.QTreeWidgetItem([file_name]) + item_name.setText(0, file_name) item_name.setIcon(0, icon) - item_name.setToolTip(0, image_file.filename) - item_name.setData(0, QtCore.Qt.UserRole, image_file) - if image_file.group_id == 0: + item_name.setToolTip(0, str(image.file_path)) + item_name.setData(0, QtCore.Qt.UserRole, image) + if image.group_id == 0: self.list_view.addTopLevelItem(item_name) else: - group_items[image_file.group_id].addChild(item_name) + group_items[image.group_id].addChild(item_name) if not initial_load: self.main_window.increment_progress_bar() if not initial_load: self.main_window.finished_progress_bar() self.application.set_normal_cursor() - def validate_and_load(self, files, target_group=None): + def validate_and_load(self, file_paths, target_group=None): """ Process a list for files either from the File Dialog or from Drag and Drop. This method is overloaded from MediaManagerItem. @@ -388,15 +389,15 @@ class ImageMediaItem(MediaManagerItem): :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ self.application.set_normal_cursor() - self.load_list(files, target_group) - last_dir = os.path.split(files[0])[0] - Settings().setValue(self.settings_section + '/last directory', Path(last_dir)) + self.load_list(file_paths, target_group) + last_dir = file_paths[0].parent + Settings().setValue(self.settings_section + '/last directory', last_dir) - def load_list(self, images, target_group=None, initial_load=False): + def load_list(self, image_paths, target_group=None, initial_load=False): """ Add new images to the database. This method is called when adding images using the Add button or DnD. - :param images: A List of strings containing the filenames of the files to be loaded + :param list[openlp.core.common.Path] image_paths: A list of file paths to the images to be loaded :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files :param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images """ @@ -429,7 +430,7 @@ class ImageMediaItem(MediaManagerItem): else: self.choose_group_form.existing_radio_button.setDisabled(False) self.choose_group_form.group_combobox.setDisabled(False) - # Ask which group the images should be saved in + # Ask which group the image_paths should be saved in if self.choose_group_form.exec(selected_group=preselect_group): if self.choose_group_form.nogroup_radio_button.isChecked(): # User chose 'No group' @@ -461,33 +462,33 @@ class ImageMediaItem(MediaManagerItem): return # Initialize busy cursor and progress bar self.application.set_busy_cursor() - self.main_window.display_progress_bar(len(images)) - # Save the new images in the database - self.save_new_images_list(images, group_id=parent_group.id, reload_list=False) - self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), + self.main_window.display_progress_bar(len(image_paths)) + # Save the new image_paths in the database + self.save_new_images_list(image_paths, group_id=parent_group.id, reload_list=False) + self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path), initial_load=initial_load, open_group=parent_group) self.application.set_normal_cursor() - def save_new_images_list(self, images_list, group_id=0, reload_list=True): + def save_new_images_list(self, image_paths, group_id=0, reload_list=True): """ Convert a list of image filenames to ImageFilenames objects and save them in the database. - :param images_list: A List of strings containing image filenames + :param list[Path] image_paths: A List of file paths to image :param group_id: The ID of the group to save the images in :param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving the new images """ - for filename in images_list: - if not isinstance(filename, str): + for image_path in image_paths: + if not isinstance(image_path, Path): continue - log.debug('Adding new image: {name}'.format(name=filename)) + log.debug('Adding new image: {name}'.format(name=image_path)) image_file = ImageFilenames() image_file.group_id = group_id - image_file.filename = str(filename) + image_file.file_path = image_path self.manager.save_object(image_file) self.main_window.increment_progress_bar() - if reload_list and images_list: - self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename)) + if reload_list and image_paths: + self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path)) def dnd_move_internal(self, target): """ @@ -581,8 +582,8 @@ class ImageMediaItem(MediaManagerItem): return False # Find missing files for image in images: - if not os.path.exists(image.filename): - missing_items_file_names.append(image.filename) + if not image.file_path.exists(): + missing_items_file_names.append(str(image.file_path)) # We cannot continue, as all images do not exist. if not images: if not remote: @@ -601,9 +602,9 @@ class ImageMediaItem(MediaManagerItem): return False # Continue with the existing images. for image in images: - name = os.path.split(image.filename)[1] - thumbnail = self.generate_thumbnail_path(image) - service_item.add_from_image(image.filename, name, background, thumbnail) + name = image.file_path.name + thumbnail_path = self.generate_thumbnail_path(image) + service_item.add_from_image(str(image.file_path), name, background, str(thumbnail_path)) return True def check_group_exists(self, new_group): @@ -640,7 +641,7 @@ class ImageMediaItem(MediaManagerItem): if not self.check_group_exists(new_group): if self.manager.save_object(new_group): self.load_full_list(self.manager.get_all_objects( - ImageFilenames, order_by_ref=ImageFilenames.filename)) + ImageFilenames, order_by_ref=ImageFilenames.file_path)) self.expand_group(new_group.id) self.fill_groups_combobox(self.choose_group_form.group_combobox) self.fill_groups_combobox(self.add_group_form.parent_group_combobox) @@ -675,9 +676,9 @@ class ImageMediaItem(MediaManagerItem): if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): # Only continue when an image is selected. return - filename = bitem.data(0, QtCore.Qt.UserRole).filename - if os.path.exists(filename): - if self.live_controller.display.direct_image(filename, background): + file_path = bitem.data(0, QtCore.Qt.UserRole).file_path + if file_path.exists(): + if self.live_controller.display.direct_image(str(file_path), background): self.reset_action.setVisible(True) else: critical_error_message_box( @@ -687,22 +688,22 @@ class ImageMediaItem(MediaManagerItem): critical_error_message_box( UiStrings().LiveBGError, translate('ImagePlugin.MediaItem', 'There was a problem replacing your background, ' - 'the image file "{name}" no longer exists.').format(name=filename)) + 'the image file "{name}" no longer exists.').format(name=file_path)) def search(self, string, show_error=True): """ Perform a search on the image file names. - :param string: The glob to search for - :param show_error: Unused. + :param str string: The glob to search for + :param bool show_error: Unused. """ files = self.manager.get_all_objects( - ImageFilenames, filter_clause=ImageFilenames.filename.contains(string), - order_by_ref=ImageFilenames.filename) + ImageFilenames, filter_clause=ImageFilenames.file_path.contains(string), + order_by_ref=ImageFilenames.file_path) results = [] for file_object in files: - filename = os.path.split(str(file_object.filename))[1] - results.append([file_object.filename, filename]) + file_name = file_object.file_path.name + results.append([str(file_object.file_path), file_name]) return results def create_item_from_id(self, item_id): @@ -711,8 +712,9 @@ class ImageMediaItem(MediaManagerItem): :param item_id: Id to make live """ + item_id = Path(item_id) item = QtWidgets.QTreeWidgetItem() - item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.filename == item_id) - item.setText(0, os.path.basename(item_data.filename)) + item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.file_path == item_id) + item.setText(0, item_data.file_path.name) item.setData(0, QtCore.Qt.UserRole, item_data) return item diff --git a/openlp/plugins/images/lib/upgrade.py b/openlp/plugins/images/lib/upgrade.py new file mode 100644 index 000000000..63690d404 --- /dev/null +++ b/openlp/plugins/images/lib/upgrade.py @@ -0,0 +1,70 @@ +# -*- 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 # +############################################################################### +""" +The :mod:`upgrade` module provides the migration path for the OLP Paths database +""" +import json +import logging + +from sqlalchemy import Column, Table + +from openlp.core.common import AppLocation +from openlp.core.common.db import drop_columns +from openlp.core.common.json import OpenLPJsonEncoder +from openlp.core.common.path import Path +from openlp.core.lib.db import PathType, get_upgrade_op + +log = logging.getLogger(__name__) +__version__ = 2 + + +def upgrade_1(session, metadata): + """ + Version 1 upgrade - old db might/might not be versioned. + """ + log.debug('Skipping upgrade_1 of files DB - not used') + + +def upgrade_2(session, metadata): + """ + Version 2 upgrade - Move file path from old db to JSON encoded path to new db. Added during 2.5 dev + """ + # TODO: Update tests + log.debug('Starting upgrade_2 for file_path to JSON') + old_table = Table('image_filenames', metadata, autoload=True) + if 'file_path' not in [col.name for col in old_table.c.values()]: + op = get_upgrade_op(session) + op.add_column('image_filenames', Column('file_path', PathType())) + conn = op.get_bind() + results = conn.execute('SELECT * FROM image_filenames') + data_path = AppLocation.get_data_path() + for row in results.fetchall(): + file_path_json = json.dumps(Path(row.filename), cls=OpenLPJsonEncoder, base_path=data_path) + sql = 'UPDATE image_filenames SET file_path = \'{file_path_json}\' WHERE id = {id}'.format( + file_path_json=file_path_json, id=row.id) + conn.execute(sql) + # Drop old columns + if metadata.bind.url.get_dialect().name == 'sqlite': + drop_columns(op, 'image_filenames', ['filename', ]) + else: + op.drop_constraint('image_filenames', 'foreignkey') + op.drop_column('image_filenames', 'filenames') diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index 821a64bb0..650179e07 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -58,7 +58,7 @@ class TestImageMediaItem(TestCase): Test that the validate_and_load_test() method when called without a group """ # GIVEN: A list of files - file_list = ['/path1/image1.jpg', '/path2/image2.jpg'] + file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')] # WHEN: Calling validate_and_load with the list of files self.media_item.validate_and_load(file_list) @@ -66,7 +66,7 @@ class TestImageMediaItem(TestCase): # THEN: load_list should have been called with the file list and None, # the directory should have been saved to the settings mocked_load_list.assert_called_once_with(file_list, None) - mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) + mocked_settings().setValue.assert_called_once_with(ANY, Path('path1')) @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list') @patch('openlp.plugins.images.lib.mediaitem.Settings') @@ -75,7 +75,7 @@ class TestImageMediaItem(TestCase): Test that the validate_and_load_test() method when called with a group """ # GIVEN: A list of files - file_list = ['/path1/image1.jpg', '/path2/image2.jpg'] + file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')] # WHEN: Calling validate_and_load with the list of files and a group self.media_item.validate_and_load(file_list, 'group') @@ -83,7 +83,7 @@ class TestImageMediaItem(TestCase): # THEN: load_list should have been called with the file list and the group name, # the directory should have been saved to the settings mocked_load_list.assert_called_once_with(file_list, 'group') - mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) + mocked_settings().setValue.assert_called_once_with(ANY, Path('path1')) @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') def test_save_new_images_list_empty_list(self, mocked_load_full_list): @@ -107,8 +107,8 @@ class TestImageMediaItem(TestCase): Test that the save_new_images_list() calls load_full_list() when reload_list is set to True """ # GIVEN: A list with 1 image and a mocked out manager - image_list = ['test_image.jpg'] - ImageFilenames.filename = '' + image_list = [Path('test_image.jpg')] + ImageFilenames.file_path = None self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with reload_list=True @@ -118,7 +118,7 @@ class TestImageMediaItem(TestCase): self.assertEquals(mocked_load_full_list.call_count, 1, 'load_full_list() should have been called') # CLEANUP: Remove added attribute from ImageFilenames - delattr(ImageFilenames, 'filename') + delattr(ImageFilenames, 'file_path') @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') def test_save_new_images_list_single_image_without_reload(self, mocked_load_full_list): @@ -126,7 +126,7 @@ class TestImageMediaItem(TestCase): Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False """ # GIVEN: A list with 1 image and a mocked out manager - image_list = ['test_image.jpg'] + image_list = [Path('test_image.jpg')] self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with reload_list=False @@ -141,7 +141,7 @@ class TestImageMediaItem(TestCase): Test that the save_new_images_list() saves all images in the list """ # GIVEN: A list with 3 images - image_list = ['test_image_1.jpg', 'test_image_2.jpg', 'test_image_3.jpg'] + image_list = [Path('test_image_1.jpg'), Path('test_image_2.jpg'), Path('test_image_3.jpg')] self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with the list of 3 images @@ -157,7 +157,7 @@ class TestImageMediaItem(TestCase): Test that the save_new_images_list() ignores everything in the provided list except strings """ # GIVEN: A list with images and objects - image_list = ['test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg'] + image_list = [Path('test_image_1.jpg'), None, True, ImageFilenames(), Path('test_image_2.jpg')] self.media_item.manager = MagicMock() # WHEN: We run save_new_images_list with the list of images and objects @@ -191,7 +191,7 @@ class TestImageMediaItem(TestCase): ImageGroups.parent_id = 1 self.media_item.manager = MagicMock() self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect - self.media_item.service_path = '' + self.media_item.service_path = Path() test_group = ImageGroups() test_group.id = 1 @@ -215,13 +215,13 @@ class TestImageMediaItem(TestCase): # Create some fake objects that should be removed returned_object1 = ImageFilenames() returned_object1.id = 1 - returned_object1.filename = '/tmp/test_file_1.jpg' + returned_object1.file_path = Path('/', 'tmp', 'test_file_1.jpg') returned_object2 = ImageFilenames() returned_object2.id = 2 - returned_object2.filename = '/tmp/test_file_2.jpg' + returned_object2.file_path = Path('/', 'tmp', 'test_file_2.jpg') returned_object3 = ImageFilenames() returned_object3.id = 3 - returned_object3.filename = '/tmp/test_file_3.jpg' + returned_object3.file_path = Path('/', 'tmp', 'test_file_3.jpg') return [returned_object1, returned_object2, returned_object3] if args[1] == ImageGroups and args[2]: # Change the parent_id that is matched so we don't get into an endless loop @@ -243,9 +243,9 @@ class TestImageMediaItem(TestCase): test_image = ImageFilenames() test_image.id = 1 test_image.group_id = 1 - test_image.filename = 'imagefile.png' + test_image.file_path = Path('imagefile.png') self.media_item.manager = MagicMock() - self.media_item.service_path = '' + self.media_item.service_path = Path() self.media_item.list_view = MagicMock() mocked_row_item = MagicMock() mocked_row_item.data.return_value = test_image @@ -265,13 +265,13 @@ class TestImageMediaItem(TestCase): # GIVEN: An ImageFilenames that already exists in the database image_file = ImageFilenames() image_file.id = 1 - image_file.filename = '/tmp/test_file_1.jpg' + image_file.file_path = Path('/', 'tmp', 'test_file_1.jpg') self.media_item.manager = MagicMock() self.media_item.manager.get_object_filtered.return_value = image_file - ImageFilenames.filename = '' + ImageFilenames.file_path = None # WHEN: create_item_from_id() is called - item = self.media_item.create_item_from_id(1) + item = self.media_item.create_item_from_id('1') # THEN: A QTreeWidgetItem should be created with the above model object as it's data self.assertIsInstance(item, QtWidgets.QTreeWidgetItem) @@ -279,4 +279,4 @@ class TestImageMediaItem(TestCase): item_data = item.data(0, QtCore.Qt.UserRole) self.assertIsInstance(item_data, ImageFilenames) self.assertEqual(1, item_data.id) - self.assertEqual('/tmp/test_file_1.jpg', item_data.filename) + self.assertEqual(Path('/', 'tmp', 'test_file_1.jpg'), item_data.file_path) diff --git a/tests/functional/openlp_plugins/images/test_upgrade.py b/tests/functional/openlp_plugins/images/test_upgrade.py new file mode 100644 index 000000000..e9f74b775 --- /dev/null +++ b/tests/functional/openlp_plugins/images/test_upgrade.py @@ -0,0 +1,83 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the lib submodule of the Images plugin. +""" +import os +import shutil +from tempfile import mkdtemp +from unittest import TestCase +from unittest.mock import patch + +from openlp.core.common import AppLocation, Settings +from openlp.core.common.path import Path +from openlp.core.lib.db import Manager +from openlp.plugins.images.lib import upgrade +from openlp.plugins.images.lib.db import ImageFilenames, init_schema + +from tests.helpers.testmixin import TestMixin +from tests.utils.constants import TEST_RESOURCES_PATH + +__default_settings__ = { + 'images/db type': 'sqlite', + 'images/background color': '#000000', +} + + +class TestImageDBUpgrade(TestCase, TestMixin): + """ + Test that the image database is upgraded correctly + """ + def setUp(self): + self.build_settings() + Settings().extend_default_settings(__default_settings__) + self.tmp_folder = mkdtemp() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + self.destroy_settings() + # Ignore errors since windows can have problems with locked files + shutil.rmtree(self.tmp_folder, ignore_errors=True) + + def test_image_filenames_table(self): + """ + Test that the ImageFilenames table is correctly upgraded to the latest version + """ + # GIVEN: An unversioned image database + temp_db_name = os.path.join(self.tmp_folder, 'image-v0.sqlite') + shutil.copyfile(os.path.join(TEST_RESOURCES_PATH, 'images', 'image-v0.sqlite'), temp_db_name) + + with patch.object(AppLocation, 'get_data_path', return_value=Path('/', 'test', 'dir')): + # WHEN: Initalising the database manager + manager = Manager('images', init_schema, db_file_path=temp_db_name, upgrade_mod=upgrade) + + # THEN: The database should have been upgraded and image_filenames.file_path should return Path objects + upgraded_results = manager.get_all_objects(ImageFilenames) + + expected_result_data = {1: Path('/', 'test', 'image1.jpg'), + 2: Path('/', 'test', 'dir', 'image2.jpg'), + 3: Path('/', 'test', 'dir', 'subdir', 'image3.jpg')} + + for result in upgraded_results: + self.assertEqual(expected_result_data[result.id], result.file_path) diff --git a/tests/resources/images/image-v0.sqlite b/tests/resources/images/image-v0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..92e199bfd44ebce41b64e4d5c1314887dd1196cb GIT binary patch literal 12288 zcmeI&O-lkn7zgm#)i8zH4uL_3<|4RQiiXg!RYsF+t#J!H*@~-d_=c`Tr?4;7SLt)~ zDVkZ^v?b3${Rd{}<(Zk?=XV>}=1tx89NHUChqgyg$sSP@a!x5BBqnM^)S$?IG#H3h zS?h1=#z=Onv?HSG?laLk;(-DI2tWV=5P$##AOHafKmY>&QDAYe#*&$g^781}oq^*H z?Y`6QxdUfp51mHf^yRLcd%HS<}$yzk+c6GU{(%`ICHRW77I8~>dRTyVxnKeH@ z$`8%_T?u`0sKk<~l(KkQOL4hknZmNK{*YX7&c@6p_S6}9q160cZ3+05T)uGTe}6cq zgf8#q;?j=-0SG_<0uX=z1Rwwb2tWV=5P-lR7LfP Date: Sun, 24 Sep 2017 09:39:54 +0100 Subject: [PATCH 24/52] Minor misc + pathlib refactors --- openlp/core/common/httputils.py | 1 - openlp/core/ui/advancedtab.py | 21 +++++----- openlp/core/ui/exceptionform.py | 14 +------ openlp/core/ui/lib/filedialog.py | 16 ++++---- openlp/plugins/alerts/forms/alertform.py | 2 +- openlp/plugins/alerts/lib/alertstab.py | 3 -- openlp/plugins/custom/lib/customtab.py | 3 -- .../songusage/forms/songusagedetaildialog.py | 1 - .../songusage/forms/songusagedetailform.py | 39 ++++++++----------- 9 files changed, 40 insertions(+), 60 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 90e128063..5bf011f17 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -24,7 +24,6 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ import hashlib import logging -import os import platform import socket import sys diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 4d9b0543f..84f0bae81 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -22,9 +22,8 @@ """ The :mod:`advancedtab` provides an advanced settings facility. """ -from datetime import datetime, timedelta import logging -import os +from datetime import datetime, timedelta from PyQt5 import QtCore, QtGui, QtWidgets @@ -492,22 +491,25 @@ class AdvancedTab(SettingsTab): self.service_name_edit.setText(UiStrings().DefaultServiceName) self.service_name_edit.setFocus() - def on_data_directory_path_edit_path_changed(self, new_data_path): + def on_data_directory_path_edit_path_changed(self, new_path): """ - Browse for a new data directory location. + Handle the `editPathChanged` signal of the data_directory_path_edit + + :param openlp.core.common.path.Path new_path: The new path + :rtype: None """ # Make sure they want to change the data. answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'), translate('OpenLP.AdvancedTab', 'Are you sure you want to change the ' 'location of the OpenLP data directory to:\n\n{path}' '\n\nThe data directory will be changed when OpenLP is ' - 'closed.').format(path=new_data_path), + 'closed.').format(path=new_path), defaultButton=QtWidgets.QMessageBox.No) if answer != QtWidgets.QMessageBox.Yes: self.data_directory_path_edit.path = AppLocation.get_data_path() return # Check if data already exists here. - self.check_data_overwrite(path_to_str(new_data_path)) + self.check_data_overwrite(new_path) # Save the new location. self.main_window.set_new_data_path(path_to_str(new_data_path)) self.data_directory_cancel_button.show() @@ -526,9 +528,10 @@ class AdvancedTab(SettingsTab): def check_data_overwrite(self, data_path): """ Check if there's already data in the target directory. + + :param openlp.core.common.path.Path data_path: The target directory to check """ - test_path = os.path.join(data_path, 'songs') - if os.path.exists(test_path): + if (data_path / 'songs').exists(): self.data_exists = True # Check is they want to replace existing data. answer = QtWidgets.QMessageBox.warning(self, @@ -537,7 +540,7 @@ class AdvancedTab(SettingsTab): 'WARNING: \n\nThe location you have selected \n\n{path}' '\n\nappears to contain OpenLP data files. Do you wish to ' 'replace these files with the current data ' - 'files?').format(path=os.path.abspath(data_path,)), + 'files?'.format(path=data_path)), QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), QtWidgets.QMessageBox.No) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 00a7c89e2..cdd64bfac 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -149,21 +149,11 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): opts = self._create_report() report_text = self.report_text.format(version=opts['version'], description=opts['description'], traceback=opts['traceback'], libs=opts['libs'], system=opts['system']) - filename = str(file_path) try: - report_file = open(filename, 'w') - try: + with file_path.open('w') as report_file: report_file.write(report_text) - except UnicodeError: - report_file.close() - report_file = open(filename, 'wb') - report_file.write(report_text.encode('utf-8')) - finally: - report_file.close() except IOError: log.exception('Failed to write crash report') - finally: - report_file.close() def on_send_report_button_clicked(self): """ @@ -219,7 +209,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), Settings().value(self.settings_section + '/last directory'), '{text} (*)'.format(text=UiStrings().AllFiles)) - log.info('New file {file}'.format(file=file_path)) + log.info('New files {file_path}'.format(file_path=file_path)) if file_path: self.file_attachment = str(file_path) diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/ui/lib/filedialog.py index 0f3ef4058..f1c2bcc24 100755 --- a/openlp/core/ui/lib/filedialog.py +++ b/openlp/core/ui/lib/filedialog.py @@ -31,11 +31,11 @@ class FileDialog(QtWidgets.QFileDialog): """ Wraps `getExistingDirectory` so that it can be called with, and return Path objects - :type parent: QtWidgets.QWidget or None + :type parent: QtWidgets.QWidget | None :type caption: str :type directory: openlp.core.common.path.Path :type options: QtWidgets.QFileDialog.Options - :rtype: tuple[Path, str] + :rtype: tuple[openlp.core.common.path.Path, str] """ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) @@ -50,13 +50,13 @@ class FileDialog(QtWidgets.QFileDialog): """ Wraps `getOpenFileName` so that it can be called with, and return Path objects - :type parent: QtWidgets.QWidget or None + :type parent: QtWidgets.QWidget | None :type caption: str :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options - :rtype: tuple[Path, str] + :rtype: tuple[openlp.core.common.path.Path, str] """ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) @@ -71,13 +71,13 @@ class FileDialog(QtWidgets.QFileDialog): """ Wraps `getOpenFileNames` so that it can be called with, and return Path objects - :type parent: QtWidgets.QWidget or None + :type parent: QtWidgets.QWidget | None :type caption: str :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options - :rtype: tuple[list[Path], str] + :rtype: tuple[list[openlp.core.common.path.Path], str] """ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) @@ -93,13 +93,13 @@ class FileDialog(QtWidgets.QFileDialog): """ Wraps `getSaveFileName` so that it can be called with, and return Path objects - :type parent: QtWidgets.QWidget or None + :type parent: QtWidgets.QWidget | None :type caption: str :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options - :rtype: tuple[Path or None, str] + :rtype: tuple[openlp.core.common.path.Path | None, str] """ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) diff --git a/openlp/plugins/alerts/forms/alertform.py b/openlp/plugins/alerts/forms/alertform.py index a9cb6cac3..a27d93390 100644 --- a/openlp/plugins/alerts/forms/alertform.py +++ b/openlp/plugins/alerts/forms/alertform.py @@ -70,7 +70,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog): item_name = QtWidgets.QListWidgetItem(alert.text) item_name.setData(QtCore.Qt.UserRole, alert.id) self.alert_list_widget.addItem(item_name) - if alert.text == str(self.alert_text_edit.text()): + if alert.text == self.alert_text_edit.text(): self.item_id = alert.id self.alert_list_widget.setCurrentRow(self.alert_list_widget.row(item_name)) diff --git a/openlp/plugins/alerts/lib/alertstab.py b/openlp/plugins/alerts/lib/alertstab.py index 1dfe0a7c3..f5934a30e 100644 --- a/openlp/plugins/alerts/lib/alertstab.py +++ b/openlp/plugins/alerts/lib/alertstab.py @@ -32,9 +32,6 @@ class AlertsTab(SettingsTab): """ AlertsTab is the alerts settings tab in the settings dialog. """ - def __init__(self, parent, name, visible_title, icon_path): - super(AlertsTab, self).__init__(parent, name, visible_title, icon_path) - def setupUi(self): self.setObjectName('AlertsTab') super(AlertsTab, self).setupUi() diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index 4ae1dab5b..167aa6d0d 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -34,9 +34,6 @@ class CustomTab(SettingsTab): """ CustomTab is the Custom settings tab in the settings dialog. """ - def __init__(self, parent, title, visible_title, icon_path): - super(CustomTab, self).__init__(parent, title, visible_title, icon_path) - def setupUi(self): self.setObjectName('CustomTab') super(CustomTab, self).setupUi() diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index 082173bf5..23ae92957 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -19,7 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - from PyQt5 import QtCore, QtWidgets from openlp.core.common import translate diff --git a/openlp/plugins/songusage/forms/songusagedetailform.py b/openlp/plugins/songusage/forms/songusagedetailform.py index 7aa636635..930baf1d6 100644 --- a/openlp/plugins/songusage/forms/songusagedetailform.py +++ b/openlp/plugins/songusage/forms/songusagedetailform.py @@ -19,7 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging import os @@ -60,7 +59,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP def on_report_path_edit_path_changed(self, file_path): """ - Called when the path in the `PathEdit` has changed + Handle the `pathEditChanged` signal from report_path_edit :param openlp.core.common.path.Path file_path: The new path. :rtype: None @@ -72,7 +71,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP Ok was triggered so lets save the data and run the report """ log.debug('accept') - path = path_to_str(self.report_path_edit.path) + path = self.report_path_edit.path if not path: self.main_window.error_message( translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'), @@ -80,7 +79,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP ' song usage report. \nPlease select an existing path on your computer.') ) return - check_directory_exists(Path(path)) + check_directory_exists(path) file_name = translate('SongUsagePlugin.SongUsageDetailForm', 'usage_detail_{old}_{new}.txt' ).format(old=self.from_date_calendar.selectedDate().toString('ddMMyyyy'), @@ -91,29 +90,25 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP SongUsageItem, and_(SongUsageItem.usagedate >= self.from_date_calendar.selectedDate().toPyDate(), SongUsageItem.usagedate < self.to_date_calendar.selectedDate().toPyDate()), [SongUsageItem.usagedate, SongUsageItem.usagetime]) - report_file_name = os.path.join(path, file_name) - file_handle = None + report_file_name = path / file_name try: - file_handle = open(report_file_name, 'wb') - for instance in usage: - record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",' - '\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime, - title=instance.title, copyright=instance.copyright, - ccli=instance.ccl_number, authors=instance.authors, - name=instance.plugin_name, source=instance.source) - file_handle.write(record.encode('utf-8')) - self.main_window.information_message( - translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'), - translate('SongUsagePlugin.SongUsageDetailForm', - 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name) - ) + with report_file_name.open('wb') as file_handle: + for instance in usage: + record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",' + '\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime, + title=instance.title, copyright=instance.copyright, + ccli=instance.ccl_number, authors=instance.authors, + name=instance.plugin_name, source=instance.source) + file_handle.write(record.encode('utf-8')) + self.main_window.information_message( + translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'), + translate('SongUsagePlugin.SongUsageDetailForm', + 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name) + ) except OSError as ose: log.exception('Failed to write out song usage records') critical_error_message_box(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation Failed'), translate('SongUsagePlugin.SongUsageDetailForm', 'An error occurred while creating the report: {error}' ).format(error=ose.strerror)) - finally: - if file_handle: - file_handle.close() self.close() From d52fff75a4164affe3037e8260cce84337bd321e Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sun, 24 Sep 2017 19:59:49 +0100 Subject: [PATCH 25/52] Test fox --- .../openlp_core_ui/test_exceptionform.py | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index 40eb19ac8..ce4c6fb81 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -22,11 +22,11 @@ """ Package to test the openlp.core.ui.exeptionform package. """ - import os import tempfile + from unittest import TestCase -from unittest.mock import mock_open, patch +from unittest.mock import call, patch from openlp.core.common import Registry from openlp.core.common.path import Path @@ -142,15 +142,15 @@ class TestExceptionForm(TestMixin, TestCase): test_form = exceptionform.ExceptionForm() test_form.file_attachment = None - with patch.object(test_form, '_pyuno_import') as mock_pyuno: - with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: - with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: - mock_pyuno.return_value = 'UNO Bridge Test' - mock_traceback.return_value = 'openlp: Traceback Test' - mock_description.return_value = 'Description Test' + with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ + patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ + patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: + mock_pyuno.return_value = 'UNO Bridge Test' + mock_traceback.return_value = 'openlp: Traceback Test' + mock_description.return_value = 'Description Test' - # WHEN: on_save_report_button_clicked called - test_form.on_send_report_button_clicked() + # WHEN: on_save_report_button_clicked called + test_form.on_send_report_button_clicked() # THEN: Verify strings were formatted properly mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT) @@ -182,25 +182,24 @@ class TestExceptionForm(TestMixin, TestCase): mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_is_linux.return_value = False mocked_application_version.return_value = 'Trunk Test' - mocked_save_filename.return_value = (Path('testfile.txt'), 'filter') - test_form = exceptionform.ExceptionForm() - test_form.file_attachment = None + with patch.object(Path, 'open') as mocked_path_open: + test_path = Path('testfile.txt') + mocked_save_filename.return_value = test_path, 'ext' - with patch.object(test_form, '_pyuno_import') as mock_pyuno: - with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: - with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: - with patch("openlp.core.ui.exceptionform.open", mock_open(), create=True) as mocked_open: - mock_pyuno.return_value = 'UNO Bridge Test' - mock_traceback.return_value = 'openlp: Traceback Test' - mock_description.return_value = 'Description Test' + test_form = exceptionform.ExceptionForm() + test_form.file_attachment = None - # WHEN: on_save_report_button_clicked called - test_form.on_save_report_button_clicked() + with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ + patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ + patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: + mock_pyuno.return_value = 'UNO Bridge Test' + mock_traceback.return_value = 'openlp: Traceback Test' + mock_description.return_value = 'Description Test' + + # WHEN: on_save_report_button_clicked called + test_form.on_save_report_button_clicked() # THEN: Verify proper calls to save file # self.maxDiff = None - check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__()) - write_text = "{text}".format(text=mocked_open.mock_calls[1]) - mocked_open.assert_called_with('testfile.txt', 'w') - self.assertEquals(check_text, write_text, "Saved information should match test text") + mocked_path_open.assert_has_calls([call().__enter__().write(MAIL_ITEM_TEXT)]) From 047a1b38089eb82adb2e0c4c5acf676911cdd0d6 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sun, 24 Sep 2017 20:33:07 +0100 Subject: [PATCH 26/52] pep fixes --- openlp/core/lib/db.py | 13 +++++++------ openlp/core/ui/advancedtab.py | 4 ++-- .../openlp_plugins/images/test_upgrade.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 9c594b14e..9d6330dc6 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -214,10 +214,11 @@ class PathType(types.TypeDecorator): """ Some times it make sense to compare a PathType with a string. In the case a string is used coerce the the PathType to a UnicodeText type. - - :param op: The operation being carried out. Not used, as we only care about the type that is being used with the - operation. - :param openlp.core.common.path.Path | str value: The value being used for the comparison. Most likely a Path Object or str. + + :param op: The operation being carried out. Not used, as we only care about the type that is being used with the + operation. + :param openlp.core.common.path.Path | str value: The value being used for the comparison. Most likely a Path + Object or str. :return: The coerced value stored in the db :rtype: PathType or UnicodeText """ @@ -343,8 +344,8 @@ class Manager(object): :param plugin_name: The name to setup paths and settings section names :param init_schema: The init_schema function for this database - :param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None resulting in the plugin_name - being used. + :param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None + resulting in the plugin_name being used. :param upgrade_mod: The upgrade_schema function for this database """ self.is_dirty = False diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 84f0bae81..e9f238c09 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -494,7 +494,7 @@ class AdvancedTab(SettingsTab): def on_data_directory_path_edit_path_changed(self, new_path): """ Handle the `editPathChanged` signal of the data_directory_path_edit - + :param openlp.core.common.path.Path new_path: The new path :rtype: None """ @@ -528,7 +528,7 @@ class AdvancedTab(SettingsTab): def check_data_overwrite(self, data_path): """ Check if there's already data in the target directory. - + :param openlp.core.common.path.Path data_path: The target directory to check """ if (data_path / 'songs').exists(): diff --git a/tests/functional/openlp_plugins/images/test_upgrade.py b/tests/functional/openlp_plugins/images/test_upgrade.py index e9f74b775..c80e9c3c9 100644 --- a/tests/functional/openlp_plugins/images/test_upgrade.py +++ b/tests/functional/openlp_plugins/images/test_upgrade.py @@ -62,7 +62,7 @@ class TestImageDBUpgrade(TestCase, TestMixin): def test_image_filenames_table(self): """ - Test that the ImageFilenames table is correctly upgraded to the latest version + Test that the ImageFilenames table is correctly upgraded to the latest version """ # GIVEN: An unversioned image database temp_db_name = os.path.join(self.tmp_folder, 'image-v0.sqlite') From 2c5205927527bc54361a21657fcdbb79f8f0dffe Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sun, 24 Sep 2017 21:26:39 +0100 Subject: [PATCH 27/52] more fixes --- openlp/core/ui/advancedtab.py | 4 ++-- openlp/core/ui/mainwindow.py | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index e9f238c09..8ec55999b 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab): # Check if data already exists here. self.check_data_overwrite(new_path) # Save the new location. - self.main_window.set_new_data_path(path_to_str(new_data_path)) + self.main_window.new_data_path = new_path self.data_directory_cancel_button.show() def on_data_directory_copy_check_box_toggled(self): @@ -562,7 +562,7 @@ class AdvancedTab(SettingsTab): """ self.data_directory_path_edit.path = AppLocation.get_data_path() self.data_directory_copy_check_box.setChecked(False) - self.main_window.set_new_data_path(None) + self.main_window.new_data_path = None self.main_window.set_copy_data(False) self.data_directory_copy_check_box.hide() self.data_directory_cancel_button.hide() diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 4b505b807..946360d87 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1332,12 +1332,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): if self.application: self.application.process_events() - def set_new_data_path(self, new_data_path): - """ - Set the new data path - """ - self.new_data_path = new_data_path - def set_copy_data(self, copy_data): """ Set the flag to copy the data @@ -1349,7 +1343,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): Change the data directory. """ log.info('Changing data path to {newpath}'.format(newpath=self.new_data_path)) - old_data_path = str(AppLocation.get_data_path()) + old_data_path = AppLocation.get_data_path() # Copy OpenLP data to new location if requested. self.application.set_busy_cursor() if self.copy_data: @@ -1358,7 +1352,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): self.show_status_message( translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} ' '- Please wait for copy to finish').format(path=self.new_data_path)) - dir_util.copy_tree(old_data_path, self.new_data_path) + dir_util.copy_tree(str(old_data_path), str(self.new_data_path)) log.info('Copy successful') except (IOError, os.error, DistutilsFileError) as why: self.application.set_normal_cursor() @@ -1373,9 +1367,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): log.info('No data copy requested') # Change the location of data directory in config file. settings = QtCore.QSettings() - settings.setValue('advanced/data path', Path(self.new_data_path)) + settings.setValue('advanced/data path', self.new_data_path) # Check if the new data path is our default. - if self.new_data_path == str(AppLocation.get_directory(AppLocation.DataDir)): + if self.new_data_path == AppLocation.get_directory(AppLocation.DataDir): settings.remove('advanced/data path') self.application.set_normal_cursor() From 286e3a6081441ad1abb0506fba7b52dc7646eb2d Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 25 Sep 2017 17:59:31 +0100 Subject: [PATCH 28/52] minor fix --- openlp/plugins/images/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 846c731fe..b436d2708 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -48,7 +48,7 @@ class ImageMediaItem(MediaManagerItem): log.info('Image Media Item loaded') def __init__(self, parent, plugin): - self.icon_resource = 'images/image' + self.icon_path = 'images/image' self.manager = None self.choose_group_form = None self.add_group_form = None From cff194d320c3c89827d00ff5f3f7edf7dedc7faa Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 25 Sep 2017 11:50:24 -0700 Subject: [PATCH 29/52] Skip Bible HTTP tests on Jenkins to prevent the server from being blacklisted --- tests/interfaces/openlp_plugins/bibles/test_lib_http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index 859957f8d..b896cd9e1 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -22,13 +22,15 @@ """ Package to test the openlp.plugin.bible.lib.https package. """ -from unittest import TestCase, skip +import os +from unittest import TestCase, skipIf from unittest.mock import MagicMock from openlp.core.common import Registry from openlp.plugins.bibles.lib.importers.http import BGExtract, CWExtract, BSExtract +@skipIf(os.environ.get('JENKINS_URL'), 'Skip Bible HTTP tests to prevent Jenkins from being blacklisted') class TestBibleHTTP(TestCase): def setUp(self): From 92551f4fa5a8d52fda4bf427c7e8ba569ab1b5e2 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 25 Sep 2017 12:01:54 -0700 Subject: [PATCH 30/52] Add requests to AppVeyor --- scripts/appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 0405d0e2c..316e5f73b 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -12,7 +12,7 @@ environment: install: # Install dependencies from pypi - - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32 pyenchant websockets asyncio waitress six webob" + - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32 pyenchant websockets asyncio waitress six webob requests" # Install mysql dependency - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df" # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) From e90836e8179dd5ef5043d2a72cc87ccb90827b5b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 25 Sep 2017 13:34:05 -0700 Subject: [PATCH 31/52] Fix up some issues with one of the tests --- openlp/core/common/httputils.py | 14 ++++++++------ .../openlp_core_common/test_httputils.py | 8 +++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 9a73659bd..a92cd92c9 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -142,7 +142,7 @@ def url_get_file(callback, url, file_path, sha256=None): :param callback: the class which needs to be updated :param url: URL to download - :param f_path: Destination file + :param file_path: Destination file :param sha256: The check sum value to be checked against the download value """ block_count = 0 @@ -151,7 +151,7 @@ def url_get_file(callback, url, file_path, sha256=None): log.debug('url_get_file: %s', url) while retries < CONNECTION_RETRIES: try: - with open(file_path, 'wb') as saved_file: + with file_path.open('wb') as saved_file: response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True) if sha256: hasher = hashlib.sha256() @@ -168,20 +168,22 @@ def url_get_file(callback, url, file_path, sha256=None): if sha256 and hasher.hexdigest() != sha256: log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(), sha256) - os.remove(file_path) + if file_path.exists(): + file_path.unlink() return False break except IOError: trace_error_handler(log) - os.remove(file_path) if retries > CONNECTION_RETRIES: + if file_path.exists(): + file_path.unlink() return False else: retries += 1 time.sleep(0.1) continue - if callback.was_cancelled: - os.remove(file_path) + if callback.was_cancelled and file_path.exists(): + file_path.unlink() return True diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 26edf4056..e620fa04e 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -228,8 +228,7 @@ class TestHttpUtils(TestCase, TestMixin): assert file_size == 100 @patch('openlp.core.common.httputils.requests') - @patch('openlp.core.common.httputils.os.remove') - def test_socket_timeout(self, mocked_remove, mocked_requests): + def test_socket_timeout(self, mocked_requests): """ Test socket timeout gets caught """ @@ -237,9 +236,8 @@ class TestHttpUtils(TestCase, TestMixin): mocked_requests.get.side_effect = IOError # WHEN: Attempt to retrieve a file - url_get_file(MagicMock(), url='http://localhost/test', file_path=self.tempfile) + url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile)) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files - mocked_remove.assert_called_with(self.tempfile) - assert mocked_remove.call_count == 3, 'os.remove() should have been called 3 times' + assert not os.path.exists(self.tempfile), 'tempfile should have been deleted' From 10b13872e50f8417caffad7375413d14199337f0 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Tue, 26 Sep 2017 17:39:13 +0100 Subject: [PATCH 32/52] Modify themes to work with pathlib --- openlp/core/lib/json/theme.json | 2 +- openlp/core/lib/renderer.py | 7 +- openlp/core/lib/theme.py | 34 +- openlp/core/ui/maindisplay.py | 8 +- openlp/core/ui/themeform.py | 50 ++- openlp/core/ui/thememanager.py | 396 ++++++++---------- openlp/core/ui/themestab.py | 4 +- openlp/plugins/images/lib/upgrade.py | 1 - .../functional/openlp_core_lib/test_theme.py | 11 +- .../openlp_core_ui/test_exceptionform.py | 50 ++- .../openlp_core_ui/test_maindisplay.py | 18 +- .../openlp_core_ui/test_themeform.py | 2 +- .../openlp_core_ui/test_thememanager.py | 51 +-- .../openlp_core_ui/test_thememanager.py | 25 +- 14 files changed, 326 insertions(+), 333 deletions(-) diff --git a/openlp/core/lib/json/theme.json b/openlp/core/lib/json/theme.json index e8862d0b4..b23593c6b 100644 --- a/openlp/core/lib/json/theme.json +++ b/openlp/core/lib/json/theme.json @@ -4,7 +4,7 @@ "color": "#000000", "direction": "vertical", "end_color": "#000000", - "filename": "", + "filename": null, "start_color": "#000000", "type": "solid" }, diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 67d33ce04..8b8d5669c 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -26,6 +26,7 @@ from string import Template from PyQt5 import QtGui, QtCore, QtWebKitWidgets from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings +from openlp.core.common.path import path_to_str from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \ build_lyrics_format_css, build_lyrics_outline_css, build_chords_css from openlp.core.common import ThemeLevel @@ -118,7 +119,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name] # if No file do not update cache if theme_data.background_filename: - self.image_manager.add_image(theme_data.background_filename, + self.image_manager.add_image(path_to_str(theme_data.background_filename), ImageSource.Theme, QtGui.QColor(theme_data.background_border_color)) def pre_render(self, override_theme_data=None): @@ -207,8 +208,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): service_item.raw_footer = FOOTER # if No file do not update cache if theme_data.background_filename: - self.image_manager.add_image( - theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color)) + self.image_manager.add_image(path_to_str(theme_data.background_filename), + ImageSource.Theme, QtGui.QColor(theme_data.background_border_color)) theme_data, main, footer = self.pre_render(theme_data) service_item.theme_data = theme_data service_item.main = main diff --git a/openlp/core/lib/theme.py b/openlp/core/lib/theme.py index 4a55b1e7e..8522225d9 100644 --- a/openlp/core/lib/theme.py +++ b/openlp/core/lib/theme.py @@ -22,13 +22,13 @@ """ Provide the theme XML and handling functions for OpenLP v2 themes. """ -import os -import logging import json +import logging from lxml import etree, objectify from openlp.core.common import AppLocation, de_hump - +from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder +from openlp.core.common.path import Path, str_to_path from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string log = logging.getLogger(__name__) @@ -160,9 +160,8 @@ class Theme(object): # basic theme object with defaults json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json' jsn = get_text_file_string(json_path) - jsn = json.loads(jsn) - self.expand_json(jsn) - self.background_filename = '' + self.load_theme(jsn) + self.background_filename = None def expand_json(self, var, prev=None): """ @@ -174,8 +173,6 @@ class Theme(object): for key, value in var.items(): if prev: key = prev + "_" + key - else: - key = key if isinstance(value, dict): self.expand_json(value, key) else: @@ -185,13 +182,13 @@ class Theme(object): """ Add the path name to the image name so the background can be rendered. - :param path: The path name to be added. + :param openlp.core.common.path.Path path: The path name to be added. + :rtype: None """ if self.background_type == 'image' or self.background_type == 'video': if self.background_filename and path: self.theme_name = self.theme_name.strip() - self.background_filename = self.background_filename.strip() - self.background_filename = os.path.join(path, self.theme_name, self.background_filename) + self.background_filename = path / self.theme_name / self.background_filename def set_default_header_footer(self): """ @@ -206,16 +203,21 @@ class Theme(object): self.font_footer_y = current_screen['size'].height() * 9 / 10 self.font_footer_height = current_screen['size'].height() / 10 - def load_theme(self, theme): + def load_theme(self, theme, theme_path=None): """ Convert the JSON file and expand it. :param theme: the theme string + :param openlp.core.common.path.Path theme_path: The path to the theme + :rtype: None """ - jsn = json.loads(theme) + if theme_path: + jsn = json.loads(theme, cls=OpenLPJsonDecoder, base_path=theme_path) + else: + jsn = json.loads(theme, cls=OpenLPJsonDecoder) self.expand_json(jsn) - def export_theme(self): + def export_theme(self, theme_path=None): """ Loop through the fields and build a dictionary of them @@ -223,7 +225,9 @@ class Theme(object): theme_data = {} for attr, value in self.__dict__.items(): theme_data["{attr}".format(attr=attr)] = value - return json.dumps(theme_data) + if theme_path: + return json.dumps(theme_data, cls=OpenLPJsonEncoder, base_path=theme_path) + return json.dumps(theme_data, cls=OpenLPJsonEncoder) def parse(self, xml): """ diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 862fcd0bc..8c8940369 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -346,7 +346,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): if not hasattr(self, 'service_item'): return False self.override['image'] = path - self.override['theme'] = self.service_item.theme_data.background_filename + self.override['theme'] = path_to_str(self.service_item.theme_data.background_filename) self.image(path) # Update the preview frame. if self.is_live: @@ -454,7 +454,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): Registry().execute('video_background_replaced') self.override = {} # We have a different theme. - elif self.override['theme'] != service_item.theme_data.background_filename: + elif self.override['theme'] != path_to_str(service_item.theme_data.background_filename): Registry().execute('live_theme_changed') self.override = {} else: @@ -466,7 +466,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): if self.service_item.theme_data.background_type == 'image': if self.service_item.theme_data.background_filename: self.service_item.bg_image_bytes = self.image_manager.get_image_bytes( - self.service_item.theme_data.background_filename, ImageSource.Theme) + path_to_str(self.service_item.theme_data.background_filename), ImageSource.Theme) if image_path: image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin) created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes, @@ -488,7 +488,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): path = os.path.join(str(AppLocation.get_section_data_path('themes')), self.service_item.theme_data.theme_name) service_item.add_from_command(path, - self.service_item.theme_data.background_filename, + path_to_str(self.service_item.theme_data.background_filename), ':/media/slidecontroller_multimedia.png') self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True) self._hide_mouse() diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index cac45f28d..d2ebaa275 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -28,7 +28,6 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file -from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm @@ -61,7 +60,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.setupUi(self) self.registerFields() self.update_theme_allowed = True - self.temp_background_filename = '' + self.temp_background_filename = None self.theme_layout_form = ThemeLayoutForm(self) self.background_combo_box.currentIndexChanged.connect(self.on_background_combo_box_current_index_changed) self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed) @@ -188,8 +187,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): """ background_image = BackgroundType.to_string(BackgroundType.Image) if self.page(self.currentId()) == self.background_page and \ - self.theme.background_type == background_image and \ - is_not_image_file(Path(self.theme.background_filename)): + self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename): QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), translate('OpenLP.ThemeWizard', 'You have not selected a ' 'background image. Please select one before continuing.')) @@ -273,7 +271,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): Run the wizard. """ log.debug('Editing theme {name}'.format(name=self.theme.theme_name)) - self.temp_background_filename = '' + self.temp_background_filename = None self.update_theme_allowed = False self.set_defaults() self.update_theme_allowed = True @@ -318,11 +316,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.setField('background_type', 1) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image): self.image_color_button.color = self.theme.background_border_color - self.image_path_edit.path = str_to_path(self.theme.background_filename) + self.image_path_edit.path = self.theme.background_filename self.setField('background_type', 2) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): self.video_color_button.color = self.theme.background_border_color - self.video_path_edit.path = str_to_path(self.theme.background_filename) + self.video_path_edit.path = self.theme.background_filename self.setField('background_type', 4) elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent): self.setField('background_type', 3) @@ -402,14 +400,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme.background_type = BackgroundType.to_string(index) if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) and \ self.theme.background_type != BackgroundType.to_string(BackgroundType.Video) and \ - self.temp_background_filename == '': + self.temp_background_filename is None: self.temp_background_filename = self.theme.background_filename - self.theme.background_filename = '' + self.theme.background_filename = None if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or self.theme.background_type != BackgroundType.to_string(BackgroundType.Video)) and \ - self.temp_background_filename != '': + self.temp_background_filename is not None: self.theme.background_filename = self.temp_background_filename - self.temp_background_filename = '' + self.temp_background_filename = None self.set_background_page_values() def on_gradient_combo_box_current_index_changed(self, index): @@ -450,18 +448,24 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): """ self.theme.background_end_color = color - def on_image_path_edit_path_changed(self, file_path): + def on_image_path_edit_path_changed(self, new_path): """ - Background Image button pushed. + Handle the `pathEditChanged` signal from image_path_edit + + :param openlp.core.common.path.Path new_path: Path to the new image + :rtype: None """ - self.theme.background_filename = path_to_str(file_path) + self.theme.background_filename = new_path self.set_background_page_values() - def on_video_path_edit_path_changed(self, file_path): + def on_video_path_edit_path_changed(self, new_path): """ - Background video button pushed. + Handle the `pathEditChanged` signal from video_path_edit + + :param openlp.core.common.path.Path new_path: Path to the new video + :rtype: None """ - self.theme.background_filename = path_to_str(file_path) + self.theme.background_filename = new_path self.set_background_page_values() def on_main_color_changed(self, color): @@ -537,14 +541,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): translate('OpenLP.ThemeWizard', 'Theme Name Invalid'), translate('OpenLP.ThemeWizard', 'Invalid theme name. Please enter one.')) return - save_from = None - save_to = None + source_path = None + destination_path = None if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \ self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): - filename = os.path.split(str(self.theme.background_filename))[1] - save_to = os.path.join(self.path, self.theme.theme_name, filename) - save_from = self.theme.background_filename + file_name = self.theme.background_filename.name + destination_path = self.path / self.theme.theme_name / file_name + source_path = self.theme.background_filename if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): return - self.theme_manager.save_theme(self.theme, save_from, save_to) + self.theme_manager.save_theme(self.theme, source_path, destination_path) return QtWidgets.QDialog.accept(self) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 15e33cdb2..7e860ffca 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -24,14 +24,14 @@ The Theme Manager manages adding, deleteing and modifying of themes. """ import os import zipfile -import shutil - from xml.etree.ElementTree import ElementTree, XML + from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ - UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file -from openlp.core.common.path import Path, path_to_str, str_to_path + UiStrings, check_directory_exists, translate, delete_file +from openlp.core.common.languagemanager import get_locale_key +from openlp.core.common.path import Path, copyfile, path_to_str, rmtree from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import Theme, BackgroundType @@ -39,7 +39,6 @@ from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.common.languagemanager import get_locale_key class Ui_ThemeManager(object): @@ -135,7 +134,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage self.settings_section = 'themes' # Variables self.theme_list = [] - self.old_background_image = None + self.old_background_image_path = None def bootstrap_initialise(self): """ @@ -145,25 +144,41 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage self.global_theme = Settings().value(self.settings_section + '/global theme') self.build_theme_path() self.load_first_time_themes() + self.upgrade_themes() def bootstrap_post_set_up(self): """ process the bootstrap post setup request """ self.theme_form = ThemeForm(self) - self.theme_form.path = self.path + self.theme_form.path = self.theme_path self.file_rename_form = FileRenameForm() Registry().register_function('theme_update_global', self.change_global_from_tab) self.load_themes() + def upgrade_themes(self): + """ + Upgrade the xml files to json. + + :rtype: None + """ + xml_file_paths = AppLocation.get_section_data_path('themes').glob('*/*.xml') + for xml_file_path in xml_file_paths: + theme_data = get_text_file_string(xml_file_path) + theme = self._create_theme_from_xml(theme_data, self.theme_path) + self._write_theme(theme) + xml_file_path.unlink() + def build_theme_path(self): """ Set up the theme path variables + + :rtype: None """ - self.path = str(AppLocation.get_section_data_path(self.settings_section)) - check_directory_exists(Path(self.path)) - self.thumb_path = os.path.join(self.path, 'thumbnails') - check_directory_exists(Path(self.thumb_path)) + self.theme_path = AppLocation.get_section_data_path(self.settings_section) + check_directory_exists(self.theme_path) + self.thumb_path = self.theme_path / 'thumbnails' + check_directory_exists(self.thumb_path) def check_list_state(self, item, field=None): """ @@ -298,17 +313,18 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage """ Takes a theme and makes a new copy of it as well as saving it. - :param theme_data: The theme to be used - :param new_theme_name: The new theme name to save the data to + :param Theme theme_data: The theme to be used + :param str new_theme_name: The new theme name of the theme + :rtype: None """ - save_to = None - save_from = None + destination_path = None + source_path = None if theme_data.background_type == 'image' or theme_data.background_type == 'video': - save_to = os.path.join(self.path, new_theme_name, os.path.split(str(theme_data.background_filename))[1]) - save_from = theme_data.background_filename + destination_path = self.theme_path / new_theme_name / theme_data.background_filename.name + source_path = theme_data.background_filename theme_data.theme_name = new_theme_name - theme_data.extend_image_filename(self.path) - self.save_theme(theme_data, save_from, save_to) + theme_data.extend_image_filename(self.theme_path) + self.save_theme(theme_data, source_path, destination_path) self.load_themes() def on_edit_theme(self, field=None): @@ -322,10 +338,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage item = self.theme_list_widget.currentItem() theme = self.get_theme_data(item.data(QtCore.Qt.UserRole)) if theme.background_type == 'image' or theme.background_type == 'video': - self.old_background_image = theme.background_filename + self.old_background_image_path = theme.background_filename self.theme_form.theme = theme self.theme_form.exec(True) - self.old_background_image = None + self.old_background_image_path = None self.renderer.update_theme(theme.theme_name) self.load_themes() @@ -355,77 +371,76 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage """ self.theme_list.remove(theme) thumb = '{name}.png'.format(name=theme) - delete_file(Path(self.path, thumb)) - delete_file(Path(self.thumb_path, thumb)) + delete_file(self.theme_path / thumb) + delete_file(self.thumb_path / thumb) try: - # Windows is always unicode, so no need to encode filenames - if is_win(): - shutil.rmtree(os.path.join(self.path, theme)) - else: - encoding = get_filesystem_encoding() - shutil.rmtree(os.path.join(self.path, theme).encode(encoding)) - except OSError as os_error: - shutil.Error = os_error + rmtree(self.theme_path / theme) + except OSError: self.log_exception('Error deleting theme {name}'.format(name=theme)) - def on_export_theme(self, field=None): + def on_export_theme(self, checked=None): """ - Export the theme in a zip file - :param field: + Export the theme to a zip file + + :param bool checked: Sent by the QAction.triggered signal. It's not used in this method. + :rtype: None """ item = self.theme_list_widget.currentItem() if item is None: critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.')) return - theme = item.data(QtCore.Qt.UserRole) + theme_name = item.data(QtCore.Qt.UserRole) export_path, filter_used = \ FileDialog.getSaveFileName(self.main_window, - translate('OpenLP.ThemeManager', 'Save Theme - ({name})'). - format(name=theme), - Settings().value(self.settings_section + '/last directory export'), - translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) + translate('OpenLP.ThemeManager', + 'Save Theme - ({name})').format(name=theme_name), + Settings().value(self.settings_section + '/last directory export'), + translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'), + translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) self.application.set_busy_cursor() if export_path: Settings().setValue(self.settings_section + '/last directory export', export_path.parent) - if self._export_theme(str(export_path), theme): + if self._export_theme(export_path.with_suffix('.otz'), theme_name): QtWidgets.QMessageBox.information(self, translate('OpenLP.ThemeManager', 'Theme Exported'), translate('OpenLP.ThemeManager', 'Your theme has been successfully exported.')) self.application.set_normal_cursor() - def _export_theme(self, theme_path, theme): + def _export_theme(self, theme_path, theme_name): """ Create the zipfile with the theme contents. - :param theme_path: Location where the zip file will be placed - :param theme: The name of the theme to be exported + + :param openlp.core.common.path.Path theme_path: Location where the zip file will be placed + :param str theme_name: The name of the theme to be exported + :return: The success of creating the zip file + :rtype: bool """ - theme_zip = None try: - theme_zip = zipfile.ZipFile(theme_path, 'w') - source = os.path.join(self.path, theme) - for files in os.walk(source): - for name in files[2]: - theme_zip.write(os.path.join(source, name), os.path.join(theme, name)) - theme_zip.close() + with zipfile.ZipFile(str(theme_path), 'w') as theme_zip: + source_path = self.theme_path / theme_name + for file_path in source_path.iterdir(): + theme_zip.write(str(file_path), os.path.join(theme_name, file_path.name)) return True except OSError as ose: self.log_exception('Export Theme Failed') critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'), - translate('OpenLP.ThemeManager', 'The theme export failed because this error ' - 'occurred: {err}').format(err=ose.strerror)) - if theme_zip: - theme_zip.close() - shutil.rmtree(theme_path, True) + translate('OpenLP.ThemeManager', + 'The theme_name export failed because this error occurred: {err}') + .format(err=ose.strerror)) + if theme_path.exists(): + rmtree(theme_path, True) return False - def on_import_theme(self, field=None): + def on_import_theme(self, checked=None): """ Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from those files. This process will only load version 2 themes. - :param field: + + :param bool checked: Sent by the QAction.triggered signal. It's not used in this method. + :rtype: None """ - file_paths, selected_filter = FileDialog.getOpenFileNames( + file_paths, filter_used = FileDialog.getOpenFileNames( self, translate('OpenLP.ThemeManager', 'Select Theme Import File'), Settings().value(self.settings_section + '/last directory import'), @@ -435,8 +450,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage return self.application.set_busy_cursor() for file_path in file_paths: - self.unzip_theme(path_to_str(file_path), self.path) - Settings().setValue(self.settings_section + '/last directory import', file_path) + self.unzip_theme(file_path, self.theme_path) + Settings().setValue(self.settings_section + '/last directory import', file_path.parent) self.load_themes() self.application.set_normal_cursor() @@ -445,17 +460,17 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage Imports any themes on start up and makes sure there is at least one theme """ self.application.set_busy_cursor() - files = AppLocation.get_files(self.settings_section, '.otz') - for theme_file in files: - theme_file = os.path.join(self.path, str(theme_file)) - self.unzip_theme(theme_file, self.path) - delete_file(Path(theme_file)) - files = AppLocation.get_files(self.settings_section, '.png') + theme_paths = AppLocation.get_files(self.settings_section, '.otz') + for theme_path in theme_paths: + theme_path = self.theme_path / theme_path + self.unzip_theme(theme_path, self.theme_path) + delete_file(theme_path) + theme_paths = AppLocation.get_files(self.settings_section, '.png') # No themes have been found so create one - if not files: + if not theme_paths: theme = Theme() theme.theme_name = UiStrings().Default - self._write_theme(theme, None, None) + self._write_theme(theme) Settings().setValue(self.settings_section + '/global theme', theme.theme_name) self.application.set_normal_cursor() @@ -471,22 +486,21 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage # Sort the themes by its name considering language specific files.sort(key=lambda file_name: get_locale_key(str(file_name))) # now process the file list of png files - for name in files: - name = str(name) + for file in files: # check to see file is in theme root directory - theme = os.path.join(self.path, name) - if os.path.exists(theme): - text_name = os.path.splitext(name)[0] + theme_path = self.theme_path / file + if theme_path.exists(): + text_name = theme_path.stem if text_name == self.global_theme: name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name) else: name = text_name - thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name)) + thumb = self.thumb_path / '{name}.png'.format(name=text_name) item_name = QtWidgets.QListWidgetItem(name) - if validate_thumb(Path(theme), Path(thumb)): + if validate_thumb(theme_path, thumb): icon = build_icon(thumb) else: - icon = create_thumb(theme, thumb) + icon = create_thumb(str(theme_path), str(thumb)) item_name.setIcon(icon) item_name.setData(QtCore.Qt.UserRole, text_name) self.theme_list_widget.addItem(item_name) @@ -507,27 +521,19 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage def get_theme_data(self, theme_name): """ - Returns a theme object from an XML or JSON file + Returns a theme object from a JSON file - :param theme_name: Name of the theme to load from file - :return: The theme object. + :param str theme_name: Name of the theme to load from file + :return: The theme object. + :rtype: Theme """ - self.log_debug('get theme data for theme {name}'.format(name=theme_name)) - theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name)) + theme_name = str(theme_name) + theme_file_path = self.theme_path / theme_name / '{file_name}.json'.format(file_name=theme_name) theme_data = get_text_file_string(theme_file_path) - jsn = True - if not theme_data: - theme_file_path = theme_file_path.with_suffix('.xml') - theme_data = get_text_file_string(theme_file_path) - jsn = False if not theme_data: self.log_debug('No theme data - using default theme') return Theme() - else: - if jsn: - return self._create_theme_from_json(theme_data, self.path) - else: - return self._create_theme_from_xml(theme_data, self.path) + return self._create_theme_from_json(theme_data, self.theme_path) def over_write_message_box(self, theme_name): """ @@ -543,172 +549,149 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage defaultButton=QtWidgets.QMessageBox.No) return ret == QtWidgets.QMessageBox.Yes - def unzip_theme(self, file_name, directory): + def unzip_theme(self, file_path, directory_path): """ Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version and upgrade if necessary. - :param file_name: - :param directory: + :param openlp.core.common.path.Path file_path: + :param openlp.core.common.path.Path directory_path: """ - self.log_debug('Unzipping theme {name}'.format(name=file_name)) - theme_zip = None - out_file = None + self.log_debug('Unzipping theme {name}'.format(name=file_path)) file_xml = None abort_import = True json_theme = False theme_name = "" try: - theme_zip = zipfile.ZipFile(file_name) - json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json'] - if len(json_file) != 1: - # TODO: remove XML handling at some point but would need a auto conversion to run first. - xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml'] - if len(xml_file) != 1: - self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file))) - raise ValidationError - xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot() - theme_version = xml_tree.get('version', default=None) - if not theme_version or float(theme_version) < 2.0: - self.log_error('Theme version is less than 2.0') - raise ValidationError - theme_name = xml_tree.find('name').text.strip() - else: - new_theme = Theme() - new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8")) - theme_name = new_theme.theme_name - json_theme = True - theme_folder = os.path.join(directory, theme_name) - theme_exists = os.path.exists(theme_folder) - if theme_exists and not self.over_write_message_box(theme_name): - abort_import = True - return - else: - abort_import = False - for name in theme_zip.namelist(): - out_name = name.replace('/', os.path.sep) - split_name = out_name.split(os.path.sep) - if split_name[-1] == '' or len(split_name) == 1: - # is directory or preview file - continue - full_name = os.path.join(directory, out_name) - check_directory_exists(Path(os.path.dirname(full_name))) - if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json': - file_xml = str(theme_zip.read(name), 'utf-8') - out_file = open(full_name, 'w', encoding='utf-8') - out_file.write(file_xml) + with zipfile.ZipFile(str(file_path)) as theme_zip: + json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json'] + if len(json_file) != 1: + # TODO: remove XML handling after the 2.6 release. + xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml'] + if len(xml_file) != 1: + self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file))) + raise ValidationError + xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot() + theme_version = xml_tree.get('version', default=None) + if not theme_version or float(theme_version) < 2.0: + self.log_error('Theme version is less than 2.0') + raise ValidationError + theme_name = xml_tree.find('name').text.strip() else: - out_file = open(full_name, 'wb') - out_file.write(theme_zip.read(name)) - out_file.close() + new_theme = Theme() + new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8")) + theme_name = new_theme.theme_name + json_theme = True + theme_folder = directory_path / theme_name + if theme_folder.exists() and not self.over_write_message_box(theme_name): + abort_import = True + return + else: + abort_import = False + for zipped_file in theme_zip.namelist(): + zipped_file_rel_path = Path(zipped_file) + split_name = zipped_file_rel_path.parts + if split_name[-1] == '' or len(split_name) == 1: + # is directory or preview file + continue + full_name = directory_path / zipped_file_rel_path + check_directory_exists(full_name.parent) + if zipped_file_rel_path.suffix.lower() == '.xml' or zipped_file_rel_path.suffix.lower() == '.json': + file_xml = str(theme_zip.read(zipped_file), 'utf-8') + with full_name.open('w', encoding='utf-8') as out_file: + out_file.write(file_xml) + else: + with full_name.open('wb') as out_file: + out_file.write(theme_zip.read(zipped_file)) except (IOError, zipfile.BadZipfile): - self.log_exception('Importing theme from zip failed {name}'.format(name=file_name)) + self.log_exception('Importing theme from zip failed {name}'.format(name=file_path)) raise ValidationError except ValidationError: critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'), translate('OpenLP.ThemeManager', 'File is not a valid theme.')) finally: - # Close the files, to be able to continue creating the theme. - if theme_zip: - theme_zip.close() - if out_file: - out_file.close() if not abort_import: # As all files are closed, we can create the Theme. if file_xml: if json_theme: - theme = self._create_theme_from_json(file_xml, self.path) + theme = self._create_theme_from_json(file_xml, self.theme_path) else: - theme = self._create_theme_from_xml(file_xml, self.path) + theme = self._create_theme_from_xml(file_xml, self.theme_path) self.generate_and_save_image(theme_name, theme) - # Only show the error message, when IOError was not raised (in - # this case the error message has already been shown). - elif theme_zip is not None: - critical_error_message_box( - translate('OpenLP.ThemeManager', 'Validation Error'), - translate('OpenLP.ThemeManager', 'File is not a valid theme.')) - self.log_error('Theme file does not contain XML data {name}'.format(name=file_name)) def check_if_theme_exists(self, theme_name): """ Check if theme already exists and displays error message - :param theme_name: Name of the Theme to test + :param str theme_name: Name of the Theme to test :return: True or False if theme exists + :rtype: bool """ - theme_dir = os.path.join(self.path, theme_name) - if os.path.exists(theme_dir): + if (self.theme_path / theme_name).exists(): critical_error_message_box( translate('OpenLP.ThemeManager', 'Validation Error'), translate('OpenLP.ThemeManager', 'A theme with this name already exists.')) return False return True - def save_theme(self, theme, image_from, image_to): + def save_theme(self, theme, image_source_path, image_destination_path): """ Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list - :param theme: The theme data object. - :param image_from: Where the theme image is currently located. - :param image_to: Where the Theme Image is to be saved to + + :param Theme theme: The theme data object. + :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located. + :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to + :rtype: None """ - self._write_theme(theme, image_from, image_to) + self._write_theme(theme, image_source_path, image_destination_path) if theme.background_type == BackgroundType.to_string(BackgroundType.Image): - self.image_manager.update_image_border(theme.background_filename, + self.image_manager.update_image_border(path_to_str(theme.background_filename), ImageSource.Theme, QtGui.QColor(theme.background_border_color)) self.image_manager.process_updates() - def _write_theme(self, theme, image_from, image_to): + def _write_theme(self, theme, image_source_path=None, image_destination_path=None): """ Writes the theme to the disk and handles the background image if necessary - :param theme: The theme data object. - :param image_from: Where the theme image is currently located. - :param image_to: Where the Theme Image is to be saved to + :param Theme theme: The theme data object. + :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located. + :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to + :rtype: None """ name = theme.theme_name - theme_pretty = theme.export_theme() - theme_dir = os.path.join(self.path, name) - check_directory_exists(Path(theme_dir)) - theme_file = os.path.join(theme_dir, name + '.json') - if self.old_background_image and image_to != self.old_background_image: - delete_file(Path(self.old_background_image)) - out_file = None + theme_pretty = theme.export_theme(self.theme_path) + theme_dir = self.theme_path / name + check_directory_exists(theme_dir) + theme_path = theme_dir / '{file_name}.json'.format(file_name=name) try: - out_file = open(theme_file, 'w', encoding='utf-8') - out_file.write(theme_pretty) + theme_path.write_text(theme_pretty) except IOError: self.log_exception('Saving theme to file failed') - finally: - if out_file: - out_file.close() - if image_from and os.path.abspath(image_from) != os.path.abspath(image_to): - try: - # Windows is always unicode, so no need to encode filenames - if is_win(): - shutil.copyfile(image_from, image_to) - else: - encoding = get_filesystem_encoding() - shutil.copyfile(image_from.encode(encoding), image_to.encode(encoding)) - except IOError as xxx_todo_changeme: - shutil.Error = xxx_todo_changeme - self.log_exception('Failed to save theme image') + if image_source_path and image_destination_path: + if self.old_background_image_path and image_destination_path != self.old_background_image_path: + delete_file(self.old_background_image_path) + if image_source_path != image_destination_path: + try: + copyfile(image_source_path, image_destination_path) + except IOError: + self.log_exception('Failed to save theme image') self.generate_and_save_image(name, theme) - def generate_and_save_image(self, name, theme): + def generate_and_save_image(self, theme_name, theme): """ Generate and save a preview image - :param name: The name of the theme. + :param str theme_name: The name of the theme. :param theme: The theme data object. """ frame = self.generate_image(theme) - sample_path_name = os.path.join(self.path, name + '.png') - if os.path.exists(sample_path_name): - os.unlink(sample_path_name) - frame.save(sample_path_name, 'png') - thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=name)) - create_thumb(sample_path_name, thumb, False) + sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name) + if sample_path_name.exists(): + sample_path_name.unlink() + frame.save(str(sample_path_name), 'png') + thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name) + create_thumb(str(sample_path_name), str(thumb_path), False) def update_preview_images(self): """ @@ -730,39 +713,32 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage """ return self.renderer.generate_preview(theme_data, force_page) - def get_preview_image(self, theme): - """ - Return an image representing the look of the theme - - :param theme: The theme to return the image for. - """ - return os.path.join(self.path, theme + '.png') - @staticmethod def _create_theme_from_xml(theme_xml, image_path): """ Return a theme object using information parsed from XML :param theme_xml: The Theme data object. - :param image_path: Where the theme image is stored + :param openlp.core.common.path.Path image_path: Where the theme image is stored :return: Theme data. + :rtype: Theme """ theme = Theme() theme.parse(theme_xml) theme.extend_image_filename(image_path) return theme - @staticmethod - def _create_theme_from_json(theme_json, image_path): + def _create_theme_from_json(self, theme_json, image_path): """ Return a theme object using information parsed from JSON :param theme_json: The Theme data object. - :param image_path: Where the theme image is stored + :param openlp.core.common.path.Path image_path: Where the theme image is stored :return: Theme data. + :rtype: Theme """ theme = Theme() - theme.load_theme(theme_json) + theme.load_theme(theme_json, self.theme_path) theme.extend_image_filename(image_path) return theme diff --git a/openlp/core/ui/themestab.py b/openlp/core/ui/themestab.py index f3b5bbb71..bf4be809c 100644 --- a/openlp/core/ui/themestab.py +++ b/openlp/core/ui/themestab.py @@ -211,8 +211,8 @@ class ThemesTab(SettingsTab): """ Utility method to update the global theme preview image. """ - image = self.theme_manager.get_preview_image(self.global_theme) - preview = QtGui.QPixmap(str(image)) + image_path = self.theme_manager.theme_path / '{file_name}.png'.format(file_name=self.global_theme) + preview = QtGui.QPixmap(str(image_path)) if not preview.isNull(): preview = preview.scaled(300, 255, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.default_list_view.setPixmap(preview) diff --git a/openlp/plugins/images/lib/upgrade.py b/openlp/plugins/images/lib/upgrade.py index 63690d404..d467e9d3c 100644 --- a/openlp/plugins/images/lib/upgrade.py +++ b/openlp/plugins/images/lib/upgrade.py @@ -48,7 +48,6 @@ def upgrade_2(session, metadata): """ Version 2 upgrade - Move file path from old db to JSON encoded path to new db. Added during 2.5 dev """ - # TODO: Update tests log.debug('Starting upgrade_2 for file_path to JSON') old_table = Table('image_filenames', metadata, autoload=True) if 'file_path' not in [col.name for col in old_table.c.values()]: diff --git a/tests/functional/openlp_core_lib/test_theme.py b/tests/functional/openlp_core_lib/test_theme.py index bb90e574a..93bc06f24 100644 --- a/tests/functional/openlp_core_lib/test_theme.py +++ b/tests/functional/openlp_core_lib/test_theme.py @@ -22,8 +22,9 @@ """ Package to test the openlp.core.lib.theme package. """ -from unittest import TestCase import os +from pathlib import Path +from unittest import TestCase from openlp.core.lib.theme import Theme @@ -79,16 +80,16 @@ class TestTheme(TestCase): """ # GIVEN: A theme object theme = Theme() - theme.theme_name = 'MyBeautifulTheme ' - theme.background_filename = ' video.mp4' + theme.theme_name = 'MyBeautifulTheme' + theme.background_filename = Path('video.mp4') theme.background_type = 'video' - path = os.path.expanduser('~') + path = Path.home() # WHEN: Theme.extend_image_filename is run theme.extend_image_filename(path) # THEN: The filename of the background should be correct - expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4') + expected_filename = path / 'MyBeautifulTheme' / 'video.mp4' self.assertEqual(expected_filename, theme.background_filename) self.assertEqual('MyBeautifulTheme', theme.theme_name) diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index 40eb19ac8..1b5c5fb59 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -22,11 +22,10 @@ """ Package to test the openlp.core.ui.exeptionform package. """ - import os import tempfile from unittest import TestCase -from unittest.mock import mock_open, patch +from unittest.mock import call, patch from openlp.core.common import Registry from openlp.core.common.path import Path @@ -142,15 +141,15 @@ class TestExceptionForm(TestMixin, TestCase): test_form = exceptionform.ExceptionForm() test_form.file_attachment = None - with patch.object(test_form, '_pyuno_import') as mock_pyuno: - with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: - with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: - mock_pyuno.return_value = 'UNO Bridge Test' - mock_traceback.return_value = 'openlp: Traceback Test' - mock_description.return_value = 'Description Test' + with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ + patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ + patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: + mock_pyuno.return_value = 'UNO Bridge Test' + mock_traceback.return_value = 'openlp: Traceback Test' + mock_description.return_value = 'Description Test' - # WHEN: on_save_report_button_clicked called - test_form.on_send_report_button_clicked() + # WHEN: on_save_report_button_clicked called + test_form.on_send_report_button_clicked() # THEN: Verify strings were formatted properly mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT) @@ -182,25 +181,24 @@ class TestExceptionForm(TestMixin, TestCase): mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_is_linux.return_value = False mocked_application_version.return_value = 'Trunk Test' - mocked_save_filename.return_value = (Path('testfile.txt'), 'filter') - test_form = exceptionform.ExceptionForm() - test_form.file_attachment = None + with patch.object(Path, 'open') as mocked_path_open: + x = Path('testfile.txt') + mocked_save_filename.return_value = x, 'ext' - with patch.object(test_form, '_pyuno_import') as mock_pyuno: - with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: - with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: - with patch("openlp.core.ui.exceptionform.open", mock_open(), create=True) as mocked_open: - mock_pyuno.return_value = 'UNO Bridge Test' - mock_traceback.return_value = 'openlp: Traceback Test' - mock_description.return_value = 'Description Test' + test_form = exceptionform.ExceptionForm() + test_form.file_attachment = None - # WHEN: on_save_report_button_clicked called - test_form.on_save_report_button_clicked() + with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ + patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ + patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: + mock_pyuno.return_value = 'UNO Bridge Test' + mock_traceback.return_value = 'openlp: Traceback Test' + mock_description.return_value = 'Description Test' + + # WHEN: on_save_report_button_clicked called + test_form.on_save_report_button_clicked() # THEN: Verify proper calls to save file # self.maxDiff = None - check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__()) - write_text = "{text}".format(text=mocked_open.mock_calls[1]) - mocked_open.assert_called_with('testfile.txt', 'w') - self.assertEquals(check_text, write_text, "Saved information should match test text") + mocked_path_open.assert_has_calls([call().__enter__().write(MAIL_ITEM_TEXT)]) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 8da2dbd55..c3f798982 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -27,10 +27,10 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtCore -from openlp.core.common import Registry, is_macosx, Settings +from openlp.core.common import Registry, is_macosx +from openlp.core.common.path import Path from openlp.core.lib import ScreenList, PluginManager from openlp.core.ui import MainDisplay, AudioPlayer -from openlp.core.ui.media import MediaController from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET from tests.helpers.testmixin import TestMixin @@ -184,7 +184,7 @@ class TestMainDisplay(TestCase, TestMixin): self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged, 'Window collection behavior should be NSWindowCollectionBehaviorManaged') - @patch(u'openlp.core.ui.maindisplay.Settings') + @patch('openlp.core.ui.maindisplay.Settings') def test_show_display_startup_logo(self, MockedSettings): # GIVEN: Mocked show_display, setting for logo visibility display = MagicMock() @@ -204,7 +204,7 @@ class TestMainDisplay(TestCase, TestMixin): # THEN: setVisible should had been called with "True" main_display.setVisible.assert_called_once_with(True) - @patch(u'openlp.core.ui.maindisplay.Settings') + @patch('openlp.core.ui.maindisplay.Settings') def test_show_display_hide_startup_logo(self, MockedSettings): # GIVEN: Mocked show_display, setting for logo visibility display = MagicMock() @@ -224,8 +224,8 @@ class TestMainDisplay(TestCase, TestMixin): # THEN: setVisible should had not been called main_display.setVisible.assert_not_called() - @patch(u'openlp.core.ui.maindisplay.Settings') - @patch(u'openlp.core.ui.maindisplay.build_html') + @patch('openlp.core.ui.maindisplay.Settings') + @patch('openlp.core.ui.maindisplay.build_html') def test_build_html_no_video(self, MockedSettings, Mocked_build_html): # GIVEN: Mocked display display = MagicMock() @@ -252,8 +252,8 @@ class TestMainDisplay(TestCase, TestMixin): self.assertEquals(main_display.media_controller.video.call_count, 0, 'Media Controller video should not have been called') - @patch(u'openlp.core.ui.maindisplay.Settings') - @patch(u'openlp.core.ui.maindisplay.build_html') + @patch('openlp.core.ui.maindisplay.Settings') + @patch('openlp.core.ui.maindisplay.build_html') def test_build_html_video(self, MockedSettings, Mocked_build_html): # GIVEN: Mocked display display = MagicMock() @@ -270,7 +270,7 @@ class TestMainDisplay(TestCase, TestMixin): service_item.theme_data = MagicMock() service_item.theme_data.background_type = 'video' service_item.theme_data.theme_name = 'name' - service_item.theme_data.background_filename = 'background_filename' + service_item.theme_data.background_filename = Path('background_filename') mocked_plugin = MagicMock() display.plugin_manager = PluginManager() display.plugin_manager.plugins = [mocked_plugin] diff --git a/tests/functional/openlp_core_ui/test_themeform.py b/tests/functional/openlp_core_ui/test_themeform.py index cff097893..e487742cc 100644 --- a/tests/functional/openlp_core_ui/test_themeform.py +++ b/tests/functional/openlp_core_ui/test_themeform.py @@ -49,5 +49,5 @@ class TestThemeManager(TestCase): self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h')) # THEN: The theme background file should be set and `set_background_page_values` should have been called - self.assertEqual(self.instance.theme.background_filename, '/new/pat.h') + self.assertEqual(self.instance.theme.background_filename, Path('/', 'new', 'pat.h')) mocked_set_background_page_values.assert_called_once_with() diff --git a/tests/functional/openlp_core_ui/test_thememanager.py b/tests/functional/openlp_core_ui/test_thememanager.py index d778fb8ef..e4b044b29 100644 --- a/tests/functional/openlp_core_ui/test_thememanager.py +++ b/tests/functional/openlp_core_ui/test_thememanager.py @@ -30,8 +30,9 @@ from unittest.mock import ANY, MagicMock, patch from PyQt5 import QtWidgets -from openlp.core.ui import ThemeManager from openlp.core.common import Registry +from openlp.core.common.path import Path +from openlp.core.ui import ThemeManager from tests.utils.constants import TEST_RESOURCES_PATH @@ -57,13 +58,13 @@ class TestThemeManager(TestCase): """ # GIVEN: A new ThemeManager instance. theme_manager = ThemeManager() - theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes') + theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes') with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \ patch('zipfile.ZipFile.write') as mocked_zipfile_write: mocked_zipfile_init.return_value = None # WHEN: The theme is exported - theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default') + theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default') # THEN: The zipfile should be created at the given path mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') @@ -86,57 +87,49 @@ class TestThemeManager(TestCase): """ Test that we don't try to overwrite a theme background image with itself """ - # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile, + # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, check_directory_exists and thememanager-attributes. - with patch('builtins.open') as mocked_open, \ - patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \ + with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ patch('openlp.core.ui.thememanager.check_directory_exists'): - mocked_open.return_value = MagicMock() theme_manager = ThemeManager(None) theme_manager.old_background_image = None theme_manager.generate_and_save_image = MagicMock() - theme_manager.path = '' + theme_manager.theme_path = MagicMock() mocked_theme = MagicMock() mocked_theme.theme_name = 'themename' mocked_theme.extract_formatted_xml = MagicMock() mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() # WHEN: Calling _write_theme with path to the same image, but the path written slightly different - file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg') - # Do replacement from end of string to avoid problems with path start - file_name2 = file_name1[::-1].replace(os.sep, os.sep + os.sep, 2)[::-1] - theme_manager._write_theme(mocked_theme, file_name1, file_name2) + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + theme_manager._write_theme(mocked_theme, file_name1, file_name1) # THEN: The mocked_copyfile should not have been called - self.assertFalse(mocked_copyfile.called, 'shutil.copyfile should not be called') + self.assertFalse(mocked_copyfile.called, 'copyfile should not be called') def test_write_theme_diff_images(self): """ Test that we do overwrite a theme background image when a new is submitted """ - # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile, + # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, check_directory_exists and thememanager-attributes. - with patch('builtins.open') as mocked_open, \ - patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \ + with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ patch('openlp.core.ui.thememanager.check_directory_exists'): - mocked_open.return_value = MagicMock() theme_manager = ThemeManager(None) theme_manager.old_background_image = None theme_manager.generate_and_save_image = MagicMock() - theme_manager.path = '' + theme_manager.theme_path = MagicMock() mocked_theme = MagicMock() mocked_theme.theme_name = 'themename' mocked_theme.filename = "filename" - # mocked_theme.extract_formatted_xml = MagicMock() - # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() # WHEN: Calling _write_theme with path to different images - file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg') - file_name2 = os.path.join(TEST_RESOURCES_PATH, 'church2.jpg') + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg') theme_manager._write_theme(mocked_theme, file_name1, file_name2) # THEN: The mocked_copyfile should not have been called - self.assertTrue(mocked_copyfile.called, 'shutil.copyfile should be called') + self.assertTrue(mocked_copyfile.called, 'copyfile should be called') def test_write_theme_special_char_name(self): """ @@ -146,7 +139,7 @@ class TestThemeManager(TestCase): theme_manager = ThemeManager(None) theme_manager.old_background_image = None theme_manager.generate_and_save_image = MagicMock() - theme_manager.path = self.temp_folder + theme_manager.theme_path = Path(self.temp_folder) mocked_theme = MagicMock() mocked_theme.theme_name = 'theme æ„› name' mocked_theme.export_theme.return_value = "{}" @@ -208,17 +201,17 @@ class TestThemeManager(TestCase): theme_manager = ThemeManager(None) theme_manager._create_theme_from_xml = MagicMock() theme_manager.generate_and_save_image = MagicMock() - theme_manager.path = '' - folder = mkdtemp() - theme_file = os.path.join(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz') + theme_manager.theme_path = None + folder = Path(mkdtemp()) + theme_file = Path(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz') # WHEN: We try to unzip it theme_manager.unzip_theme(theme_file, folder) # THEN: Files should be unpacked - self.assertTrue(os.path.exists(os.path.join(folder, 'Moss on tree', 'Moss on tree.xml'))) + self.assertTrue((folder / 'Moss on tree' / 'Moss on tree.xml').exists()) self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened') - shutil.rmtree(folder) + shutil.rmtree(str(folder)) def test_unzip_theme_invalid_version(self): """ diff --git a/tests/interfaces/openlp_core_ui/test_thememanager.py b/tests/interfaces/openlp_core_ui/test_thememanager.py index b560df154..0797aa9b8 100644 --- a/tests/interfaces/openlp_core_ui/test_thememanager.py +++ b/tests/interfaces/openlp_core_ui/test_thememanager.py @@ -26,7 +26,8 @@ from unittest import TestCase from unittest.mock import patch, MagicMock from openlp.core.common import Registry, Settings -from openlp.core.ui import ThemeManager, ThemeForm, FileRenameForm +from openlp.core.common.path import Path +from openlp.core.ui import ThemeManager from tests.helpers.testmixin import TestMixin @@ -91,6 +92,23 @@ class TestThemeManager(TestCase, TestMixin): assert self.theme_manager.thumb_path.startswith(self.theme_manager.path) is True, \ 'The thumb path and the main path should start with the same value' + def test_build_theme_path(self): + """ + Test the thememanager build_theme_path - basic test + """ + # GIVEN: A new a call to initialise + with patch('openlp.core.common.AppLocation.get_section_data_path', return_value=Path('test/path')): + Settings().setValue('themes/global theme', 'my_theme') + + self.theme_manager.theme_form = MagicMock() + self.theme_manager.load_first_time_themes = MagicMock() + + # WHEN: the build_theme_path is run + self.theme_manager.build_theme_path() + + # THEN: The thumbnail path should be a sub path of the test path + self.assertEqual(self.theme_manager.thumb_path, Path('test/path/thumbnails')) + def test_click_on_new_theme(self): """ Test the on_add_theme event handler is called by the UI @@ -109,17 +127,16 @@ class TestThemeManager(TestCase, TestMixin): @patch('openlp.core.ui.themeform.ThemeForm._setup') @patch('openlp.core.ui.filerenameform.FileRenameForm._setup') - def test_bootstrap_post(self, mocked_theme_form, mocked_rename_form): + def test_bootstrap_post(self, mocked_rename_form, mocked_theme_form): """ Test the functions of bootstrap_post_setup are called. """ # GIVEN: self.theme_manager.load_themes = MagicMock() - self.theme_manager.path = MagicMock() + self.theme_manager.theme_path = MagicMock() # WHEN: self.theme_manager.bootstrap_post_set_up() # THEN: - self.assertEqual(self.theme_manager.path, self.theme_manager.theme_form.path) self.assertEqual(1, self.theme_manager.load_themes.call_count, "load_themes should have been called once") From dfcd95b9d91bc45104b9261d4cc41e295b47e491 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Tue, 26 Sep 2017 18:02:56 +0100 Subject: [PATCH 33/52] pep fixes --- openlp/core/ui/themeform.py | 4 ++-- openlp/core/ui/thememanager.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index d2ebaa275..37978d10a 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -451,7 +451,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): def on_image_path_edit_path_changed(self, new_path): """ Handle the `pathEditChanged` signal from image_path_edit - + :param openlp.core.common.path.Path new_path: Path to the new image :rtype: None """ @@ -461,7 +461,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): def on_video_path_edit_path_changed(self, new_path): """ Handle the `pathEditChanged` signal from video_path_edit - + :param openlp.core.common.path.Path new_path: Path to the new video :rtype: None """ diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 7e860ffca..dffa82d47 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -392,11 +392,11 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage theme_name = item.data(QtCore.Qt.UserRole) export_path, filter_used = \ FileDialog.getSaveFileName(self.main_window, - translate('OpenLP.ThemeManager', - 'Save Theme - ({name})').format(name=theme_name), - Settings().value(self.settings_section + '/last directory export'), - translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'), - translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) + translate('OpenLP.ThemeManager', + 'Save Theme - ({name})').format(name=theme_name), + Settings().value(self.settings_section + '/last directory export'), + translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'), + translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) self.application.set_busy_cursor() if export_path: Settings().setValue(self.settings_section + '/last directory export', export_path.parent) @@ -637,7 +637,6 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage """ Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list - :param Theme theme: The theme data object. :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located. :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to From 86cf77db83e7f98f0f7d23955ce6a4c52bf35d06 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 18:21:54 +0100 Subject: [PATCH 34/52] add returns --- openlp/plugins/bibles/endpoint.py | 4 ++-- openlp/plugins/custom/endpoint.py | 4 ++-- openlp/plugins/images/endpoint.py | 4 ++-- openlp/plugins/media/endpoint.py | 4 ++-- openlp/plugins/presentations/endpoint.py | 4 ++-- openlp/plugins/songs/endpoint.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/bibles/endpoint.py b/openlp/plugins/bibles/endpoint.py index 08a945e7c..4d34eb966 100644 --- a/openlp/plugins/bibles/endpoint.py +++ b/openlp/plugins/bibles/endpoint.py @@ -62,7 +62,7 @@ def bibles_service(request): :param request: The http request object. """ - service(request, 'bibles', log) + return service(request, 'bibles', log) @api_bibles_endpoint.route('bibles/search') @@ -95,6 +95,6 @@ def bibles_service_api(request): :param request: The http request object. """ try: - search(request, 'bibles', log) + return search(request, 'bibles', log) except NotFound: return {'results': {'items': []}} diff --git a/openlp/plugins/custom/endpoint.py b/openlp/plugins/custom/endpoint.py index 687ffaa1b..ca5c39088 100644 --- a/openlp/plugins/custom/endpoint.py +++ b/openlp/plugins/custom/endpoint.py @@ -62,7 +62,7 @@ def custom_service(request): :param request: The http request object. """ - service(request, 'custom', log) + return service(request, 'custom', log) @api_custom_endpoint.route('custom/search') @@ -95,6 +95,6 @@ def custom_service_api(request): :param request: The http request object. """ try: - search(request, 'custom', log) + return search(request, 'custom', log) except NotFound: return {'results': {'items': []}} diff --git a/openlp/plugins/images/endpoint.py b/openlp/plugins/images/endpoint.py index ca82da00a..05c1e64b4 100644 --- a/openlp/plugins/images/endpoint.py +++ b/openlp/plugins/images/endpoint.py @@ -75,7 +75,7 @@ def images_service(request): :param request: The http request object. """ - service(request, 'images', log) + return service(request, 'images', log) @api_images_endpoint.route('images/search') @@ -108,6 +108,6 @@ def images_service_api(request): :param request: The http request object. """ try: - search(request, 'images', log) + return search(request, 'images', log) except NotFound: return {'results': {'items': []}} diff --git a/openlp/plugins/media/endpoint.py b/openlp/plugins/media/endpoint.py index 014c3c790..c7b703358 100644 --- a/openlp/plugins/media/endpoint.py +++ b/openlp/plugins/media/endpoint.py @@ -62,7 +62,7 @@ def media_service(request): :param request: The http request object. """ - service(request, 'media', log) + return service(request, 'media', log) @api_media_endpoint.route('media/search') @@ -95,6 +95,6 @@ def media_service_api(request): :param request: The http request object. """ try: - search(request, 'media', log) + return search(request, 'media', log) except NotFound: return {'results': {'items': []}} diff --git a/openlp/plugins/presentations/endpoint.py b/openlp/plugins/presentations/endpoint.py index ce622083c..99c828b4b 100644 --- a/openlp/plugins/presentations/endpoint.py +++ b/openlp/plugins/presentations/endpoint.py @@ -76,7 +76,7 @@ def presentations_service(request): :param request: The http request object. """ - service(request, 'presentations', log) + return service(request, 'presentations', log) @api_presentations_endpoint.route('presentations/search') @@ -109,6 +109,6 @@ def presentations_service_api(request): :param request: The http request object. """ try: - search(request, 'presentations', log) + return search(request, 'presentations', log) except NotFound: return {'results': {'items': []}} diff --git a/openlp/plugins/songs/endpoint.py b/openlp/plugins/songs/endpoint.py index 8711fcccd..6bd521872 100644 --- a/openlp/plugins/songs/endpoint.py +++ b/openlp/plugins/songs/endpoint.py @@ -62,7 +62,7 @@ def songs_service(request): :param request: The http request object. """ - service(request, 'songs', log) + return service(request, 'songs', log) @api_songs_endpoint.route('songs/search') @@ -95,6 +95,6 @@ def songs_service_api(request): :param request: The http request object. """ try: - search(request, 'songs', log) + return search(request, 'songs', log) except NotFound: return {'results': {'items': []}} From 9fc7f1b39b3d1b5df488050e192185518d671844 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 18:48:01 +0100 Subject: [PATCH 35/52] restart removal --- openlp/plugins/remotes/endpoint.py | 46 -------------------------- openlp/plugins/remotes/remoteplugin.py | 7 ++-- 2 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 openlp/plugins/remotes/endpoint.py diff --git a/openlp/plugins/remotes/endpoint.py b/openlp/plugins/remotes/endpoint.py deleted file mode 100644 index a9b0d0815..000000000 --- a/openlp/plugins/remotes/endpoint.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- 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 # -############################################################################### -import logging - -import os - -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.endpoint.core import TRANSLATED_STRINGS -from openlp.core.common import AppLocation - - -static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes'))) - -log = logging.getLogger(__name__) - -remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir) - - -@remote_endpoint.route('{view}') -def index(request, view): - """ - Handles requests for /remotes url - - :param request: The http request object. - :param view: The view name to be servered. - """ - return remote_endpoint.render_template('{view}.mako'.format(view=view), **TRANSLATED_STRINGS) diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 1344c66d1..4974e451b 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -20,16 +20,15 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import logging -import os import time - from PyQt5 import QtCore, QtWidgets +import logging + +from openlp.core.api.endpoint.remote import remote_endpoint from openlp.core.api.http import register_endpoint from openlp.core.common import AppLocation, Registry, Settings, OpenLPMixin, UiStrings, check_directory_exists from openlp.core.lib import Plugin, StringContent, translate, build_icon -from openlp.plugins.remotes.endpoint import remote_endpoint from openlp.plugins.remotes.deploy import download_and_check, download_sha256 log = logging.getLogger(__name__) From 5b2ac0f97b7f655c47e33eb00e308ac9ea862abb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 18:49:41 +0100 Subject: [PATCH 36/52] more moves --- openlp/plugins/remotes/deploy.py | 69 ------------------- openlp/plugins/remotes/remoteplugin.py | 6 +- .../openlp_plugins/remotes/test_deploy.py | 4 +- 3 files changed, 4 insertions(+), 75 deletions(-) delete mode 100644 openlp/plugins/remotes/deploy.py diff --git a/openlp/plugins/remotes/deploy.py b/openlp/plugins/remotes/deploy.py deleted file mode 100644 index 44c628837..000000000 --- a/openlp/plugins/remotes/deploy.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- 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 # -############################################################################### -""" -Download and "install" the remote web client -""" -import os -from zipfile import ZipFile - -from openlp.core.common import AppLocation, Registry -from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size - - -def deploy_zipfile(app_root, zip_name): - """ - Process the downloaded zip file and add to the correct directory - - :param zip_name: the zip file to be processed - :param app_root: the directory where the zip get expanded to - - :return: None - """ - zip_file = os.path.join(app_root, zip_name) - web_zip = ZipFile(zip_file) - web_zip.extractall(app_root) - - -def download_sha256(): - """ - Download the config file to extract the sha256 and version number - """ - user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() - try: - web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent}) - except ConnectionError: - return False - file_bits = web_config.split() - return file_bits[0], file_bits[2] - - -def download_and_check(callback=None): - """ - Download the web site and deploy it. - """ - sha256, version = download_sha256() - file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') - callback.setRange(0, file_size) - if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), - AppLocation.get_section_data_path('remotes') / 'site.zip', - sha256=sha256): - deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 4974e451b..c223eb99e 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -20,16 +20,16 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +import logging import time + from PyQt5 import QtCore, QtWidgets -import logging - +from openlp.core.api.deploy import download_and_check, download_sha256 from openlp.core.api.endpoint.remote import remote_endpoint from openlp.core.api.http import register_endpoint from openlp.core.common import AppLocation, Registry, Settings, OpenLPMixin, UiStrings, check_directory_exists from openlp.core.lib import Plugin, StringContent, translate, build_icon -from openlp.plugins.remotes.deploy import download_and_check, download_sha256 log = logging.getLogger(__name__) __default_settings__ = { diff --git a/tests/functional/openlp_plugins/remotes/test_deploy.py b/tests/functional/openlp_plugins/remotes/test_deploy.py index 1909b94fd..14447a97e 100644 --- a/tests/functional/openlp_plugins/remotes/test_deploy.py +++ b/tests/functional/openlp_plugins/remotes/test_deploy.py @@ -22,12 +22,10 @@ import os import shutil - from tempfile import mkdtemp from unittest import TestCase -from openlp.plugins.remotes.deploy import deploy_zipfile - +from openlp.core.api.deploy import deploy_zipfile TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) From 64ba30b24dced2acb9fabf32f143bf28891736a7 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 18:51:09 +0100 Subject: [PATCH 37/52] add files --- openlp/core/api/deploy.py | 69 ++++++++++++++++++++++++++++++ openlp/core/api/endpoint/remote.py | 46 ++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 openlp/core/api/deploy.py create mode 100644 openlp/core/api/endpoint/remote.py diff --git a/openlp/core/api/deploy.py b/openlp/core/api/deploy.py new file mode 100644 index 000000000..44c628837 --- /dev/null +++ b/openlp/core/api/deploy.py @@ -0,0 +1,69 @@ +# -*- 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 # +############################################################################### +""" +Download and "install" the remote web client +""" +import os +from zipfile import ZipFile + +from openlp.core.common import AppLocation, Registry +from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size + + +def deploy_zipfile(app_root, zip_name): + """ + Process the downloaded zip file and add to the correct directory + + :param zip_name: the zip file to be processed + :param app_root: the directory where the zip get expanded to + + :return: None + """ + zip_file = os.path.join(app_root, zip_name) + web_zip = ZipFile(zip_file) + web_zip.extractall(app_root) + + +def download_sha256(): + """ + Download the config file to extract the sha256 and version number + """ + user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() + try: + web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent}) + except ConnectionError: + return False + file_bits = web_config.split() + return file_bits[0], file_bits[2] + + +def download_and_check(callback=None): + """ + Download the web site and deploy it. + """ + sha256, version = download_sha256() + file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') + callback.setRange(0, file_size) + if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), + AppLocation.get_section_data_path('remotes') / 'site.zip', + sha256=sha256): + deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') diff --git a/openlp/core/api/endpoint/remote.py b/openlp/core/api/endpoint/remote.py new file mode 100644 index 000000000..a9b0d0815 --- /dev/null +++ b/openlp/core/api/endpoint/remote.py @@ -0,0 +1,46 @@ +# -*- 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 # +############################################################################### +import logging + +import os + +from openlp.core.api.http.endpoint import Endpoint +from openlp.core.api.endpoint.core import TRANSLATED_STRINGS +from openlp.core.common import AppLocation + + +static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes'))) + +log = logging.getLogger(__name__) + +remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir) + + +@remote_endpoint.route('{view}') +def index(request, view): + """ + Handles requests for /remotes url + + :param request: The http request object. + :param view: The view name to be servered. + """ + return remote_endpoint.render_template('{view}.mako'.format(view=view), **TRANSLATED_STRINGS) From b783783a297362f1200f44b73b50311c2b978e00 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 21:33:58 +0100 Subject: [PATCH 38/52] add files --- .../openlp_plugins/remotes/__init__.py | 21 ------- .../openlp_plugins/remotes/test_deploy.py | 62 ------------------- 2 files changed, 83 deletions(-) delete mode 100644 tests/functional/openlp_plugins/remotes/__init__.py delete mode 100644 tests/functional/openlp_plugins/remotes/test_deploy.py diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py deleted file mode 100644 index ea62548f4..000000000 --- a/tests/functional/openlp_plugins/remotes/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- 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 # -############################################################################### diff --git a/tests/functional/openlp_plugins/remotes/test_deploy.py b/tests/functional/openlp_plugins/remotes/test_deploy.py deleted file mode 100644 index 14447a97e..000000000 --- a/tests/functional/openlp_plugins/remotes/test_deploy.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- 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 # -############################################################################### - -import os -import shutil -from tempfile import mkdtemp -from unittest import TestCase - -from openlp.core.api.deploy import deploy_zipfile - -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) - - -class TestRemoteDeploy(TestCase): - """ - Test the Remote plugin deploy functions - """ - - def setUp(self): - """ - Setup for tests - """ - self.app_root = mkdtemp() - - def tearDown(self): - """ - Clean up after tests - """ - shutil.rmtree(self.app_root) - - def test_deploy_zipfile(self): - """ - Remote Deploy tests - test the dummy zip file is processed correctly - """ - # GIVEN: A new downloaded zip file - zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip') - app_root = os.path.join(self.app_root, 'site.zip') - shutil.copyfile(zip_file, app_root) - # WHEN: I process the zipfile - deploy_zipfile(self.app_root, 'site.zip') - - # THEN test if www directory has been created - self.assertTrue(os.path.isdir(os.path.join(self.app_root, 'www')), 'We should have a www directory') From 4451b65f04c2f6490fdf30902a3e3942d2565e8e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 21:34:08 +0100 Subject: [PATCH 39/52] add files2 --- .../functional/openlp_core_api/test_deploy.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/functional/openlp_core_api/test_deploy.py diff --git a/tests/functional/openlp_core_api/test_deploy.py b/tests/functional/openlp_core_api/test_deploy.py new file mode 100644 index 000000000..14447a97e --- /dev/null +++ b/tests/functional/openlp_core_api/test_deploy.py @@ -0,0 +1,62 @@ +# -*- 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 # +############################################################################### + +import os +import shutil +from tempfile import mkdtemp +from unittest import TestCase + +from openlp.core.api.deploy import deploy_zipfile + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) + + +class TestRemoteDeploy(TestCase): + """ + Test the Remote plugin deploy functions + """ + + def setUp(self): + """ + Setup for tests + """ + self.app_root = mkdtemp() + + def tearDown(self): + """ + Clean up after tests + """ + shutil.rmtree(self.app_root) + + def test_deploy_zipfile(self): + """ + Remote Deploy tests - test the dummy zip file is processed correctly + """ + # GIVEN: A new downloaded zip file + zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip') + app_root = os.path.join(self.app_root, 'site.zip') + shutil.copyfile(zip_file, app_root) + # WHEN: I process the zipfile + deploy_zipfile(self.app_root, 'site.zip') + + # THEN test if www directory has been created + self.assertTrue(os.path.isdir(os.path.join(self.app_root, 'www')), 'We should have a www directory') From f58d1047db8f66ded0b82605495c63e266635b4f Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 22:04:39 +0100 Subject: [PATCH 40/52] move endpoint --- openlp/core/api/endpoint/remote.py | 7 +------ openlp/core/api/endpoint/service.py | 2 +- openlp/core/api/http/server.py | 3 +++ openlp/plugins/songs/endpoint.py | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openlp/core/api/endpoint/remote.py b/openlp/core/api/endpoint/remote.py index a9b0d0815..4741ada15 100644 --- a/openlp/core/api/endpoint/remote.py +++ b/openlp/core/api/endpoint/remote.py @@ -21,18 +21,13 @@ ############################################################################### import logging -import os - from openlp.core.api.http.endpoint import Endpoint from openlp.core.api.endpoint.core import TRANSLATED_STRINGS -from openlp.core.common import AppLocation -static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes'))) - log = logging.getLogger(__name__) -remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir) +remote_endpoint = Endpoint('remote', template_dir='remotes', static_dir='remotes') @remote_endpoint.route('{view}') diff --git a/openlp/core/api/endpoint/service.py b/openlp/core/api/endpoint/service.py index acb139b43..4e3b53fbb 100644 --- a/openlp/core/api/endpoint/service.py +++ b/openlp/core/api/endpoint/service.py @@ -23,7 +23,7 @@ import logging import json from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http import register_endpoint, requires_auth +from openlp.core.api.http import requires_auth from openlp.core.common import Registry diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index a7ec34903..28d883487 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -37,6 +37,8 @@ from openlp.core.api.poll import Poller from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint +from openlp.core.api.endpoint.remote import remote_endpoint + log = logging.getLogger(__name__) @@ -95,3 +97,4 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): register_endpoint(main_endpoint) register_endpoint(service_endpoint) register_endpoint(api_service_endpoint) + register_endpoint(remote_endpoint) diff --git a/openlp/plugins/songs/endpoint.py b/openlp/plugins/songs/endpoint.py index 6bd521872..c8af62764 100644 --- a/openlp/plugins/songs/endpoint.py +++ b/openlp/plugins/songs/endpoint.py @@ -95,6 +95,6 @@ def songs_service_api(request): :param request: The http request object. """ try: - return search(request, 'songs', log) + return service(request, 'songs', log) except NotFound: return {'results': {'items': []}} From 74cdc761d9459eed5398a9b32a956bce3881d050 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Sep 2017 22:18:09 +0100 Subject: [PATCH 41/52] end of remote and server migration --- openlp/core/api/http/server.py | 88 +++++++++++++- openlp/core/common/settings.py | 1 + openlp/plugins/remotes/__init__.py | 21 ---- openlp/plugins/remotes/remoteplugin.py | 154 ------------------------- 4 files changed, 87 insertions(+), 177 deletions(-) delete mode 100644 openlp/plugins/remotes/__init__.py delete mode 100644 openlp/plugins/remotes/remoteplugin.py diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index 28d883487..9ba3a1bb0 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -26,13 +26,18 @@ with OpenLP. It uses JSON to communicate with the remotes. """ import logging +import time -from PyQt5 import QtCore +from PyQt5 import QtCore, QtWidgets from waitress import serve from openlp.core.api.http import register_endpoint from openlp.core.api.http import application -from openlp.core.common import RegistryMixin, RegistryProperties, OpenLPMixin, Settings, Registry +from openlp.core.common import AppLocation, RegistryMixin, RegistryProperties, OpenLPMixin, \ + Settings, Registry, UiStrings, check_directory_exists +from openlp.core.lib import translate + +from openlp.core.api.deploy import download_and_check, download_sha256 from openlp.core.api.poll import Poller from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint @@ -81,11 +86,15 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.thread.start() + Registry().register_function('download_website', self.first_time) + Registry().register_function('get_website_version', self.website_version) + Registry().set_flag('website_version', '0001_01_01') def bootstrap_post_set_up(self): """ Register the poll return service and start the servers. """ + self.initialise() self.poller = Poller() Registry().register('poller', self.poller) application.initialise() @@ -98,3 +107,78 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): register_endpoint(service_endpoint) register_endpoint(api_service_endpoint) register_endpoint(remote_endpoint) + + @staticmethod + def initialise(): + """ + Create the internal file structure if it does not exist + :return: + """ + check_directory_exists(AppLocation.get_section_data_path('remotes') / 'assets') + check_directory_exists(AppLocation.get_section_data_path('remotes') / 'images') + check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static') + check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static' / 'index') + check_directory_exists(AppLocation.get_section_data_path('remotes') / 'templates') + + def first_time(self): + """ + Import web site code if active + """ + self.application.process_events() + progress = Progress(self) + progress.forceShow() + self.application.process_events() + time.sleep(1) + download_and_check(progress) + self.application.process_events() + time.sleep(1) + progress.close() + self.application.process_events() + Settings().setValue('remotes/download version', self.version) + + def website_version(self): + """ + Download and save the website version and sha256 + :return: None + """ + sha256, self.version = download_sha256() + Registry().set_flag('website_sha256', sha256) + Registry().set_flag('website_version', self.version) + + +class Progress(QtWidgets.QProgressDialog): + """ + Local class to handle download display based and supporting httputils:get_web_page + """ + def __init__(self, parent): + super(Progress, self).__init__(parent.main_window) + self.parent = parent + self.setWindowModality(QtCore.Qt.WindowModal) + self.setWindowTitle(translate('RemotePlugin', 'Importing Website')) + self.setLabelText(UiStrings().StartingImport) + self.setCancelButton(None) + self.setRange(0, 1) + self.setMinimumDuration(0) + self.was_cancelled = False + self.previous_size = 0 + + def _download_progress(self, count, block_size): + """ + Calculate and display the download progress. + """ + increment = (count * block_size) - self.previous_size + self._increment_progress_bar(None, increment) + self.previous_size = count * block_size + + def _increment_progress_bar(self, status_text, increment=1): + """ + Update the wizard progress page. + + :param status_text: Current status information to display. + :param increment: The value to increment the progress bar by. + """ + if status_text: + self.setText(status_text) + if increment > 0: + self.setValue(self.value() + increment) + self.parent.application.process_events() \ No newline at end of file diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index be1b29c75..477925280 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -177,6 +177,7 @@ class Settings(QtCore.QSettings): 'images/background color': '#000000', 'media/players': 'system,webkit', 'media/override player': QtCore.Qt.Unchecked, + 'remotes/download version': '0000_00_00', 'players/background color': '#000000', 'servicemanager/last directory': None, 'servicemanager/last file': None, diff --git a/openlp/plugins/remotes/__init__.py b/openlp/plugins/remotes/__init__.py deleted file mode 100644 index ea62548f4..000000000 --- a/openlp/plugins/remotes/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- 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 # -############################################################################### diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py deleted file mode 100644 index c223eb99e..000000000 --- a/openlp/plugins/remotes/remoteplugin.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- 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 # -############################################################################### - -import logging -import time - -from PyQt5 import QtCore, QtWidgets - -from openlp.core.api.deploy import download_and_check, download_sha256 -from openlp.core.api.endpoint.remote import remote_endpoint -from openlp.core.api.http import register_endpoint -from openlp.core.common import AppLocation, Registry, Settings, OpenLPMixin, UiStrings, check_directory_exists -from openlp.core.lib import Plugin, StringContent, translate, build_icon - -log = logging.getLogger(__name__) -__default_settings__ = { - 'remotes/download version': '0000_00_00' -} - - -class RemotesPlugin(Plugin, OpenLPMixin): - log.info('Remotes Plugin loaded') - - def __init__(self): - """ - remotes constructor - """ - super(RemotesPlugin, self).__init__('remotes', __default_settings__, {}) - self.icon_path = ':/plugins/plugin_remote.png' - self.icon = build_icon(self.icon_path) - self.weight = -1 - register_endpoint(remote_endpoint) - Registry().register_function('download_website', self.first_time) - Registry().register_function('get_website_version', self.website_version) - Registry().set_flag('website_version', '0001_01_01') - - def initialise(self): - """ - Create the internal file structure if it does not exist - :return: - """ - check_directory_exists(AppLocation.get_section_data_path('remotes') / 'assets') - check_directory_exists(AppLocation.get_section_data_path('remotes') / 'images') - check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static') - check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static', 'index') - check_directory_exists(AppLocation.get_section_data_path('remotes') / 'templates') - - @staticmethod - def about(): - """ - Information about this plugin - """ - about_text = translate( - 'RemotePlugin', - 'Web Interface' - '
The web interface plugin provides the ability to develop web based interfaces using OpenLP web ' - 'services.\nPredefined interfaces can be download as well as custom developed interfaces.') - return about_text - - def set_plugin_text_strings(self): - """ - Called to define all translatable texts of the plugin - """ - # Name PluginList - self.text_strings[StringContent.Name] = { - 'singular': translate('RemotePlugin', 'Web Interface', 'name singular'), - 'plural': translate('RemotePlugin', 'Web Interface', 'name plural') - } - # Name for MediaDockManager, SettingsManager - self.text_strings[StringContent.VisibleName] = { - 'title': translate('RemotePlugin', 'Web Remote', 'container title') - } - - def first_time(self): - """ - Import web site code if active - """ - self.application.process_events() - progress = Progress(self) - progress.forceShow() - self.application.process_events() - time.sleep(1) - download_and_check(progress) - self.application.process_events() - time.sleep(1) - progress.close() - self.application.process_events() - Settings().setValue('remotes/download version', self.version) - - def website_version(self): - """ - Download and save the website version and sha256 - :return: None - """ - sha256, self.version = download_sha256() - Registry().set_flag('website_sha256', sha256) - Registry().set_flag('website_version', self.version) - - -class Progress(QtWidgets.QProgressDialog): - """ - Local class to handle download display based and supporting httputils:get_web_page - """ - def __init__(self, parent): - super(Progress, self).__init__(parent.main_window) - self.parent = parent - self.setWindowModality(QtCore.Qt.WindowModal) - self.setWindowTitle(translate('RemotePlugin', 'Importing Website')) - self.setLabelText(UiStrings().StartingImport) - self.setCancelButton(None) - self.setRange(0, 1) - self.setMinimumDuration(0) - self.was_cancelled = False - self.previous_size = 0 - - def _download_progress(self, count, block_size): - """ - Calculate and display the download progress. - """ - increment = (count * block_size) - self.previous_size - self._increment_progress_bar(None, increment) - self.previous_size = count * block_size - - def _increment_progress_bar(self, status_text, increment=1): - """ - Update the wizard progress page. - - :param status_text: Current status information to display. - :param increment: The value to increment the progress bar by. - """ - if status_text: - self.setText(status_text) - if increment > 0: - self.setValue(self.value() + increment) - self.parent.application.process_events() From 71a87f602b1011e47005c98542b6c485e683bfd8 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 27 Sep 2017 19:56:21 +0100 Subject: [PATCH 42/52] fix download date --- openlp/core/api/deploy.py | 2 +- openlp/core/api/http/server.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/core/api/deploy.py b/openlp/core/api/deploy.py index 44c628837..9b1e6e793 100644 --- a/openlp/core/api/deploy.py +++ b/openlp/core/api/deploy.py @@ -63,7 +63,7 @@ def download_and_check(callback=None): sha256, version = download_sha256() file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') callback.setRange(0, file_size) - if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), + if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip', AppLocation.get_section_data_path('remotes') / 'site.zip', sha256=sha256): deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index 9ba3a1bb0..55f9a2305 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -66,6 +66,7 @@ class HttpWorker(QtCore.QObject): """ address = Settings().value('api/ip address') port = Settings().value('api/port') + Registry().execute('get_website_version') serve(application, host=address, port=port) def stop(self): From bfc8a5ae8c52fb0d994ce20b1f611f97fb6e7449 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 27 Sep 2017 20:12:22 +0100 Subject: [PATCH 43/52] fix tests --- tests/functional/openlp_core_api/test_deploy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_api/test_deploy.py b/tests/functional/openlp_core_api/test_deploy.py index 14447a97e..273894b99 100644 --- a/tests/functional/openlp_core_api/test_deploy.py +++ b/tests/functional/openlp_core_api/test_deploy.py @@ -27,7 +27,7 @@ from unittest import TestCase from openlp.core.api.deploy import deploy_zipfile -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) class TestRemoteDeploy(TestCase): @@ -52,6 +52,7 @@ class TestRemoteDeploy(TestCase): Remote Deploy tests - test the dummy zip file is processed correctly """ # GIVEN: A new downloaded zip file + aa = TEST_PATH zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip') app_root = os.path.join(self.app_root, 'site.zip') shutil.copyfile(zip_file, app_root) From e2fd8beb2a0090271a85475048835c74c88246fe Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 27 Sep 2017 20:16:24 +0100 Subject: [PATCH 44/52] fix tests --- tests/interfaces/openlp_core_lib/test_pluginmanager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core_lib/test_pluginmanager.py index 2e9e8342f..3dbae0e03 100644 --- a/tests/interfaces/openlp_core_lib/test_pluginmanager.py +++ b/tests/interfaces/openlp_core_lib/test_pluginmanager.py @@ -94,4 +94,3 @@ class TestPluginManager(TestCase, TestMixin): self.assertIn('custom', plugin_names, 'There should be a "custom" plugin') self.assertIn('songusage', plugin_names, 'There should be a "songusage" plugin') self.assertIn('alerts', plugin_names, 'There should be a "alerts" plugin') - self.assertIn('remotes', plugin_names, 'There should be a "remotes" plugin') From 0c1b1dc2a5f1f5cc4b4852fea06793463a982328 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 27 Sep 2017 20:21:08 +0100 Subject: [PATCH 45/52] fix tests --- openlp/core/api/http/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index 55f9a2305..916aedddd 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -182,4 +182,4 @@ class Progress(QtWidgets.QProgressDialog): self.setText(status_text) if increment > 0: self.setValue(self.value() + increment) - self.parent.application.process_events() \ No newline at end of file + self.parent.application.process_events() From 93fd4053b2e81184bead35035feebfb085fa3731 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 28 Sep 2017 10:31:28 -0700 Subject: [PATCH 46/52] Add dark theme from QDarkStyle. Part 1: Force the theme --- openlp/core/__init__.py | 19 ++++++++++++------- openlp/core/ui/dark.py | 32 ++++++++++++++++++++++++++++++++ openlp/core/ui/mainwindow.py | 4 +++- 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 openlp/core/ui/dark.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 88b3dbfb7..c49ccff95 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -45,11 +45,13 @@ from openlp.core.common.versionchecker import VersionThread, get_application_ver from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen +from openlp.core.ui.dark import HAS_DARK_STYLE, DARK_STYLESHEET from openlp.core.ui.exceptionform import ExceptionForm from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm from openlp.core.ui.mainwindow import MainWindow + __all__ = ['OpenLP', 'main'] @@ -122,13 +124,16 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): sys.exit() # Correct stylesheet bugs application_stylesheet = '' - if not Settings().value('advanced/alternate rows'): - base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) - alternate_rows_repair_stylesheet = \ - 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' - application_stylesheet += alternate_rows_repair_stylesheet - if is_win(): - application_stylesheet += WIN_REPAIR_STYLESHEET + if HAS_DARK_STYLE: + application_stylesheet = DARK_STYLESHEET + else: + if not Settings().value('advanced/alternate rows'): + base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) + alternate_rows_repair_stylesheet = \ + 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' + application_stylesheet += alternate_rows_repair_stylesheet + if is_win(): + application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) can_show_splash = Settings().value('core/show splash') diff --git a/openlp/core/ui/dark.py b/openlp/core/ui/dark.py new file mode 100644 index 000000000..762c1ee4a --- /dev/null +++ b/openlp/core/ui/dark.py @@ -0,0 +1,32 @@ +# -*- 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 # +############################################################################### +""" +The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme +""" + +try: + import qdarkstyle + HAS_DARK_STYLE = True + DARK_STYLESHEET = qdarkstyle.load_stylesheet_pyqt5() +except: + HAS_DARK_STYLE = False + DARK_STYLESHEET = '' diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 413e97a17..33a9ec2ca 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -45,6 +45,7 @@ from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController +from openlp.core.ui.dark import HAS_DARK_STYLE from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm @@ -155,7 +156,8 @@ class Ui_MainWindow(object): # Create the MediaManager self.media_manager_dock = OpenLPDockWidget(main_window, 'media_manager_dock', ':/system/system_mediamanager.png') - self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE) + if not HAS_DARK_STYLE: + self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE) # Create the media toolbox self.media_tool_box = QtWidgets.QToolBox(self.media_manager_dock) self.media_tool_box.setObjectName('media_tool_box') From 1b7482243da71ce8b248b146a2c38e60b5a1e797 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 28 Sep 2017 16:28:18 -0700 Subject: [PATCH 47/52] Centralise style tweaks --- openlp/core/__init__.py | 36 +------- openlp/core/common/settings.py | 1 + openlp/core/ui/advancedtab.py | 152 ++++++++++++++++++--------------- openlp/core/ui/dark.py | 32 ------- openlp/core/ui/mainwindow.py | 25 +----- openlp/core/ui/style.py | 119 ++++++++++++++++++++++++++ 6 files changed, 206 insertions(+), 159 deletions(-) delete mode 100644 openlp/core/ui/dark.py create mode 100644 openlp/core/ui/style.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index ef67ec745..c9e446143 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -42,11 +42,11 @@ from openlp.core.version import check_for_update, get_version from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen -from openlp.core.ui.dark import HAS_DARK_STYLE, DARK_STYLESHEET from openlp.core.ui.exceptionform import ExceptionForm from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm from openlp.core.ui.mainwindow import MainWindow +from openlp.core.ui.style import get_application_stylesheet __all__ = ['OpenLP', 'main'] @@ -54,28 +54,6 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -WIN_REPAIR_STYLESHEET = """ -QMainWindow::separator -{ - border: none; -} - -QDockWidget::title -{ - border: 1px solid palette(dark); - padding-left: 5px; - padding-top: 2px; - margin: 1px 0; -} - -QToolBar -{ - border: none; - margin: 0; - padding: 0; -} -""" - class OpenLP(OpenLPMixin, QtWidgets.QApplication): """ @@ -120,17 +98,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): QtCore.QCoreApplication.exit() sys.exit() # Correct stylesheet bugs - application_stylesheet = '' - if HAS_DARK_STYLE: - application_stylesheet = DARK_STYLESHEET - else: - if not Settings().value('advanced/alternate rows'): - base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) - alternate_rows_repair_stylesheet = \ - 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' - application_stylesheet += alternate_rows_repair_stylesheet - if is_win(): - application_stylesheet += WIN_REPAIR_STYLESHEET + application_stylesheet = get_application_stylesheet() if application_stylesheet: self.setStyleSheet(application_stylesheet) can_show_splash = Settings().value('core/show splash') diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index be1b29c75..0540ff81e 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -136,6 +136,7 @@ class Settings(QtCore.QSettings): 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, 'advanced/search as type': True, + 'advanced/use_dark_style': False, 'api/twelve hour': True, 'api/port': 4316, 'api/websocket port': 4317, diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 8ec55999b..b655b3899 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -32,6 +32,7 @@ from openlp.core.common.languagemanager import format_time from openlp.core.common.path import path_to_str from openlp.core.lib import SettingsTab, build_icon from openlp.core.ui.lib import PathEdit, PathType +from openlp.core.ui.style import HAS_DARK_STYLE log = logging.getLogger(__name__) @@ -109,8 +110,80 @@ class AdvancedTab(SettingsTab): self.enable_auto_close_check_box.setObjectName('enable_auto_close_check_box') self.ui_layout.addRow(self.enable_auto_close_check_box) self.left_layout.addWidget(self.ui_group_box) + if HAS_DARK_STYLE: + self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box) + self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox') + self.ui_layout.addRow(self.use_dark_style_checkbox) + # Data Directory + self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column) + self.data_directory_group_box.setObjectName('data_directory_group_box') + self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box) + self.data_directory_layout.setObjectName('data_directory_layout') + self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box) + self.data_directory_new_label.setObjectName('data_directory_current_label') + self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories, + default_path=AppLocation.get_directory(AppLocation.DataDir)) + self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit) + self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box) + self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label') + self.new_data_directory_has_files_label.setWordWrap(True) + self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box) + self.data_directory_cancel_button.setObjectName('data_directory_cancel_button') + self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png')) + self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout() + self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout') + self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box) + self.data_directory_copy_check_box.setObjectName('data_directory_copy_check_box') + self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box) + self.data_directory_copy_check_layout.addStretch() + self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button) + self.data_directory_layout.addRow(self.data_directory_copy_check_layout) + self.data_directory_layout.addRow(self.new_data_directory_has_files_label) + self.left_layout.addWidget(self.data_directory_group_box) + # Hide mouse + self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column) + self.hide_mouse_group_box.setObjectName('hide_mouse_group_box') + self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box) + self.hide_mouse_layout.setObjectName('hide_mouse_layout') + self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box) + self.hide_mouse_check_box.setObjectName('hide_mouse_check_box') + self.hide_mouse_layout.addWidget(self.hide_mouse_check_box) + self.right_layout.addWidget(self.hide_mouse_group_box) + # Service Item Slide Limits + self.slide_group_box = QtWidgets.QGroupBox(self.right_column) + self.slide_group_box.setObjectName('slide_group_box') + self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box) + self.slide_layout.setObjectName('slide_layout') + self.slide_label = QtWidgets.QLabel(self.slide_group_box) + self.slide_label.setWordWrap(True) + self.slide_layout.addWidget(self.slide_label) + self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box) + self.end_slide_radio_button.setObjectName('end_slide_radio_button') + self.slide_layout.addWidget(self.end_slide_radio_button) + self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box) + self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button') + self.slide_layout.addWidget(self.wrap_slide_radio_button) + self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box) + self.next_item_radio_button.setObjectName('next_item_radio_button') + self.slide_layout.addWidget(self.next_item_radio_button) + self.right_layout.addWidget(self.slide_group_box) + # Display Workarounds + self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column) + self.display_workaround_group_box.setObjectName('display_workaround_group_box') + self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box) + self.display_workaround_layout.setObjectName('display_workaround_layout') + self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) + self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box') + self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box) + self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) + self.x11_bypass_check_box.setObjectName('x11_bypass_check_box') + self.display_workaround_layout.addWidget(self.x11_bypass_check_box) + self.alternate_rows_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) + self.alternate_rows_check_box.setObjectName('alternate_rows_check_box') + self.display_workaround_layout.addWidget(self.alternate_rows_check_box) + self.right_layout.addWidget(self.display_workaround_group_box) # Default service name - self.service_name_group_box = QtWidgets.QGroupBox(self.left_column) + self.service_name_group_box = QtWidgets.QGroupBox(self.right_column) self.service_name_group_box.setObjectName('service_name_group_box') self.service_name_layout = QtWidgets.QFormLayout(self.service_name_group_box) self.service_name_check_box = QtWidgets.QCheckBox(self.service_name_group_box) @@ -147,77 +220,11 @@ class AdvancedTab(SettingsTab): self.service_name_example = QtWidgets.QLabel(self.service_name_group_box) self.service_name_example.setObjectName('service_name_example') self.service_name_layout.addRow(self.service_name_example_label, self.service_name_example) - self.left_layout.addWidget(self.service_name_group_box) - # Data Directory - self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column) - self.data_directory_group_box.setObjectName('data_directory_group_box') - self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box) - self.data_directory_layout.setObjectName('data_directory_layout') - self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box) - self.data_directory_new_label.setObjectName('data_directory_current_label') - self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories, - default_path=AppLocation.get_directory(AppLocation.DataDir)) - self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit) - self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box) - self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label') - self.new_data_directory_has_files_label.setWordWrap(True) - self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box) - self.data_directory_cancel_button.setObjectName('data_directory_cancel_button') - self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png')) - self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout() - self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout') - self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box) - self.data_directory_copy_check_box.setObjectName('data_directory_copy_check_box') - self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box) - self.data_directory_copy_check_layout.addStretch() - self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button) - self.data_directory_layout.addRow(self.data_directory_copy_check_layout) - self.data_directory_layout.addRow(self.new_data_directory_has_files_label) - self.left_layout.addWidget(self.data_directory_group_box) + self.right_layout.addWidget(self.service_name_group_box) + # After the last item on each side, add some spacing self.left_layout.addStretch() - # Hide mouse - self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column) - self.hide_mouse_group_box.setObjectName('hide_mouse_group_box') - self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box) - self.hide_mouse_layout.setObjectName('hide_mouse_layout') - self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box) - self.hide_mouse_check_box.setObjectName('hide_mouse_check_box') - self.hide_mouse_layout.addWidget(self.hide_mouse_check_box) - self.right_layout.addWidget(self.hide_mouse_group_box) - # Service Item Slide Limits - self.slide_group_box = QtWidgets.QGroupBox(self.right_column) - self.slide_group_box.setObjectName('slide_group_box') - self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box) - self.slide_layout.setObjectName('slide_layout') - self.slide_label = QtWidgets.QLabel(self.slide_group_box) - self.slide_label.setWordWrap(True) - self.slide_layout.addWidget(self.slide_label) - self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box) - self.end_slide_radio_button.setObjectName('end_slide_radio_button') - self.slide_layout.addWidget(self.end_slide_radio_button) - self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box) - self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button') - self.slide_layout.addWidget(self.wrap_slide_radio_button) - self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box) - self.next_item_radio_button.setObjectName('next_item_radio_button') - self.slide_layout.addWidget(self.next_item_radio_button) - self.right_layout.addWidget(self.slide_group_box) - # Display Workarounds - self.display_workaround_group_box = QtWidgets.QGroupBox(self.left_column) - self.display_workaround_group_box.setObjectName('display_workaround_group_box') - self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box) - self.display_workaround_layout.setObjectName('display_workaround_layout') - self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) - self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box') - self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box) - self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) - self.x11_bypass_check_box.setObjectName('x11_bypass_check_box') - self.display_workaround_layout.addWidget(self.x11_bypass_check_box) - self.alternate_rows_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box) - self.alternate_rows_check_box.setObjectName('alternate_rows_check_box') - self.display_workaround_layout.addWidget(self.alternate_rows_check_box) - self.right_layout.addWidget(self.display_workaround_group_box) self.right_layout.addStretch() + # Set up all the connections and things self.should_update_service_name_example = False self.service_name_check_box.toggled.connect(self.service_name_check_box_toggled) self.service_name_day.currentIndexChanged.connect(self.on_service_name_day_changed) @@ -282,6 +289,7 @@ class AdvancedTab(SettingsTab): 'Auto-scroll the next slide to bottom')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) + self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) self.service_name_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable default service name')) self.service_name_time_label.setText(translate('OpenLP.AdvancedTab', 'Date and Time:')) @@ -349,6 +357,8 @@ class AdvancedTab(SettingsTab): if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count(): self.autoscroll_combo_box.setCurrentIndex(i) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) + if HAS_DARK_STYLE: + self.use_dark_style_checkbox.setChecked(settings.value('use_dark_style')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) self.service_name_day.setCurrentIndex(settings.value('default service day')) self.service_name_time.setTime(QtCore.QTime(settings.value('default service hour'), @@ -420,6 +430,8 @@ class AdvancedTab(SettingsTab): self.settings_form.register_post_process('config_screen_changed') self.settings_form.register_post_process('slidecontroller_update_slide_limits') settings.setValue('search as type', self.is_search_as_you_type_enabled) + if HAS_DARK_STYLE: + settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked()) settings.endGroup() def on_search_as_type_check_box_changed(self, check_state): diff --git a/openlp/core/ui/dark.py b/openlp/core/ui/dark.py deleted file mode 100644 index 762c1ee4a..000000000 --- a/openlp/core/ui/dark.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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 # -############################################################################### -""" -The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme -""" - -try: - import qdarkstyle - HAS_DARK_STYLE = True - DARK_STYLESHEET = qdarkstyle.load_stylesheet_pyqt5() -except: - HAS_DARK_STYLE = False - DARK_STYLESHEET = '' diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 077883513..25fe818ee 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -44,7 +44,6 @@ from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController -from openlp.core.ui.dark import HAS_DARK_STYLE from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm @@ -52,31 +51,12 @@ from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.lib.dockwidget import OpenLPDockWidget from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.ui.lib.mediadockmanager import MediaDockManager +from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet from openlp.core.version import get_version log = logging.getLogger(__name__) -MEDIA_MANAGER_STYLE = """ -::tab#media_tool_box { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 palette(button), stop: 1.0 palette(mid)); - border: 0; - border-radius: 2px; - margin-top: 0; - margin-bottom: 0; - text-align: left; -} -/* This is here to make the tabs on KDE with the Breeze theme work */ -::tab:selected {} -""" - -PROGRESSBAR_STYLE = """ -QProgressBar{ - height: 10px; -} -""" - class Ui_MainWindow(object): """ @@ -156,8 +136,7 @@ class Ui_MainWindow(object): # Create the MediaManager self.media_manager_dock = OpenLPDockWidget(main_window, 'media_manager_dock', ':/system/system_mediamanager.png') - if not HAS_DARK_STYLE: - self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE) + self.media_manager_dock.setStyleSheet(get_library_stylesheet()) # Create the media toolbox self.media_tool_box = QtWidgets.QToolBox(self.media_manager_dock) self.media_tool_box.setObjectName('media_tool_box') diff --git a/openlp/core/ui/style.py b/openlp/core/ui/style.py new file mode 100644 index 000000000..7ffeb9533 --- /dev/null +++ b/openlp/core/ui/style.py @@ -0,0 +1,119 @@ +# -*- 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 # +############################################################################### +""" +The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme +""" +from PyQt5 import QtGui + +from openlp.core.common import is_macosx, is_win +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings + +try: + import qdarkstyle + HAS_DARK_STYLE = True +except ImportError: + HAS_DARK_STYLE = False + +WIN_REPAIR_STYLESHEET = """ +QMainWindow::separator +{ + border: none; +} + +QDockWidget::title +{ + border: 1px solid palette(dark); + padding-left: 5px; + padding-top: 2px; + margin: 1px 0; +} + +QToolBar +{ + border: none; + margin: 0; + padding: 0; +} +""" + +MEDIA_MANAGER_STYLE = """ +::tab#media_tool_box { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 palette(button), stop: 1.0 palette(mid)); + border: 0; + border-radius: 2px; + margin-top: 0; + margin-bottom: 0; + text-align: left; +} +/* This is here to make the tabs on KDE with the Breeze theme work */ +::tab:selected {} +""" + +PROGRESSBAR_STYLE = """ +QProgressBar{ + height: 10px; +} +""" + + +def can_show_icon(): + """ + A global method to determine if an icon can be shown on a widget + + .. note:: + This method uses internal imports to prevent circular imports. + """ + return not is_macosx() or Settings.value('advanced/use_dark_style') + + +def get_application_stylesheet(): + """ + Return the correct application stylesheet based on the current style and operating system + + :return str: The correct stylesheet as a string + """ + stylesheet = '' + if HAS_DARK_STYLE and Settings().value('advanced/use_dark_style'): + stylesheet = qdarkstyle.load_stylesheet_pyqt5() + else: + if not Settings().value('advanced/alternate rows'): + base_color = Registry().get('application').palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) + alternate_rows_repair_stylesheet = \ + 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' + stylesheet += alternate_rows_repair_stylesheet + if is_win(): + stylesheet += WIN_REPAIR_STYLESHEET + return stylesheet + + +def get_library_stylesheet(): + """ + Return the correct stylesheet for the main window + + :return str: The correct stylesheet as a string + """ + if not HAS_DARK_STYLE or not Settings().value('advanced/use_dark_style'): + return MEDIA_MANAGER_STYLE + else: + return '' From 77b9fee05010497be35188ea5ac4df1c8485ec3d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 28 Sep 2017 16:39:44 -0700 Subject: [PATCH 48/52] Checkbox is only there is there is a dark style --- openlp/core/ui/advancedtab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index b655b3899..26303f1cb 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -289,7 +289,8 @@ class AdvancedTab(SettingsTab): 'Auto-scroll the next slide to bottom')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) - self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) + if HAS_DARK_STYLE: + self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) self.service_name_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable default service name')) self.service_name_time_label.setText(translate('OpenLP.AdvancedTab', 'Date and Time:')) From 76bf162383d48d0f53bad07da8777b2dd650748e Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 28 Sep 2017 16:50:23 -0700 Subject: [PATCH 49/52] Fix error with Settings().value() call --- openlp/core/ui/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/style.py b/openlp/core/ui/style.py index 7ffeb9533..71a7e5c15 100644 --- a/openlp/core/ui/style.py +++ b/openlp/core/ui/style.py @@ -84,7 +84,7 @@ def can_show_icon(): .. note:: This method uses internal imports to prevent circular imports. """ - return not is_macosx() or Settings.value('advanced/use_dark_style') + return not is_macosx() or Settings().value('advanced/use_dark_style') def get_application_stylesheet(): From 0dcf3de267bf726ee0029fae0edf946b94a00a3d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 28 Sep 2017 20:06:00 -0700 Subject: [PATCH 50/52] Tests! --- openlp/core/ui/style.py | 10 -- tests/functional/openlp_core_ui/test_style.py | 110 ++++++++++++++++++ 2 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 tests/functional/openlp_core_ui/test_style.py diff --git a/openlp/core/ui/style.py b/openlp/core/ui/style.py index 71a7e5c15..764a3bd26 100644 --- a/openlp/core/ui/style.py +++ b/openlp/core/ui/style.py @@ -77,16 +77,6 @@ QProgressBar{ """ -def can_show_icon(): - """ - A global method to determine if an icon can be shown on a widget - - .. note:: - This method uses internal imports to prevent circular imports. - """ - return not is_macosx() or Settings().value('advanced/use_dark_style') - - def get_application_stylesheet(): """ Return the correct application stylesheet based on the current style and operating system diff --git a/tests/functional/openlp_core_ui/test_style.py b/tests/functional/openlp_core_ui/test_style.py new file mode 100644 index 000000000..8bdfea047 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_style.py @@ -0,0 +1,110 @@ +# -*- 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 # +############################################################################### +""" +Package to test the :mod:`~openlp.core.ui.style` module. +""" +from unittest.mock import MagicMock, patch + +from openlp.core.ui.style import MEDIA_MANAGER_STYLE, WIN_REPAIR_STYLESHEET, get_application_stylesheet, \ + get_library_stylesheet + + +@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) +@patch('openlp.core.ui.style.Settings') +@patch('openlp.core.ui.style.qdarkstyle') +def test_get_application_stylesheet_dark(mocked_qdarkstyle, MockSettings): + """Test that the dark stylesheet is returned when available and enabled""" + # GIVEN: We're on Windows and no dark style is set + mocked_settings = MagicMock() + mocked_settings.value.return_value = True + MockSettings.return_value = mocked_settings + mocked_qdarkstyle.load_stylesheet_pyqt5.return_value = 'dark_style' + + # WHEN: can_show_icon() is called + result = get_application_stylesheet() + + # THEN: the result should be false + assert result == 'dark_style' + + +@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) +@patch('openlp.core.ui.style.is_win') +@patch('openlp.core.ui.style.Settings') +@patch('openlp.core.ui.style.Registry') +def test_get_application_stylesheet_not_alternate_rows(MockRegistry, MockSettings, mocked_is_win): + """Test that the alternate rows stylesheet is returned when enabled in settings""" + # GIVEN: We're on Windows and no dark style is set + mocked_is_win.return_value = False + MockSettings.return_value.value.return_value = False + MockRegistry.return_value.get.return_value.palette.return_value.color.return_value.name.return_value = 'color' + + # WHEN: can_show_icon() is called + result = get_application_stylesheet() + + # THEN: the result should be false + MockSettings.return_value.value.assert_called_once_with('advanced/alternate rows') + assert result == 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: color;}\n', result + + +@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) +@patch('openlp.core.ui.style.is_win') +@patch('openlp.core.ui.style.Settings') +def test_get_application_stylesheet_win_repair(MockSettings, mocked_is_win): + """Test that the Windows repair stylesheet is returned when on Windows""" + # GIVEN: We're on Windows and no dark style is set + mocked_is_win.return_value = True + MockSettings.return_value.value.return_value = True + + # WHEN: can_show_icon() is called + result = get_application_stylesheet() + + # THEN: the result should be false + MockSettings.return_value.value.assert_called_once_with('advanced/alternate rows') + assert result == WIN_REPAIR_STYLESHEET + + +@patch('openlp.core.ui.style.HAS_DARK_STYLE', False) +@patch('openlp.core.ui.style.Settings') +def test_get_library_stylesheet_no_dark_style(MockSettings): + """Test that the media manager stylesheet is returned when there's no dark theme available""" + # GIVEN: No dark style + MockSettings.return_value.value.return_value = False + + # WHEN: get_library_stylesheet() is called + result = get_library_stylesheet() + + # THEN: The correct stylesheet should be returned + assert result == MEDIA_MANAGER_STYLE + + +@patch('openlp.core.ui.style.HAS_DARK_STYLE', True) +@patch('openlp.core.ui.style.Settings') +def test_get_library_stylesheet_dark_style(MockSettings): + """Test that no stylesheet is returned when the dark theme is enabled""" + # GIVEN: No dark style + MockSettings.return_value.value.return_value = True + + # WHEN: get_library_stylesheet() is called + result = get_library_stylesheet() + + # THEN: The correct stylesheet should be returned + assert result == '' From 11fbad77dc23c3036135bd5465110624bc7f5f12 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 29 Sep 2017 08:55:39 -0700 Subject: [PATCH 51/52] Fix some linting issues --- tests/functional/openlp_core_ui/test_style.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_style.py b/tests/functional/openlp_core_ui/test_style.py index 8bdfea047..7435df1c7 100644 --- a/tests/functional/openlp_core_ui/test_style.py +++ b/tests/functional/openlp_core_ui/test_style.py @@ -24,13 +24,14 @@ Package to test the :mod:`~openlp.core.ui.style` module. """ from unittest.mock import MagicMock, patch +import openlp.core.ui.style from openlp.core.ui.style import MEDIA_MANAGER_STYLE, WIN_REPAIR_STYLESHEET, get_application_stylesheet, \ get_library_stylesheet @patch('openlp.core.ui.style.HAS_DARK_STYLE', True) @patch('openlp.core.ui.style.Settings') -@patch('openlp.core.ui.style.qdarkstyle') +@patch.object(openlp.core.ui.style, 'qdarkstyle') def test_get_application_stylesheet_dark(mocked_qdarkstyle, MockSettings): """Test that the dark stylesheet is returned when available and enabled""" # GIVEN: We're on Windows and no dark style is set @@ -38,7 +39,7 @@ def test_get_application_stylesheet_dark(mocked_qdarkstyle, MockSettings): mocked_settings.value.return_value = True MockSettings.return_value = mocked_settings mocked_qdarkstyle.load_stylesheet_pyqt5.return_value = 'dark_style' - + # WHEN: can_show_icon() is called result = get_application_stylesheet() @@ -56,7 +57,7 @@ def test_get_application_stylesheet_not_alternate_rows(MockRegistry, MockSetting mocked_is_win.return_value = False MockSettings.return_value.value.return_value = False MockRegistry.return_value.get.return_value.palette.return_value.color.return_value.name.return_value = 'color' - + # WHEN: can_show_icon() is called result = get_application_stylesheet() @@ -73,7 +74,7 @@ def test_get_application_stylesheet_win_repair(MockSettings, mocked_is_win): # GIVEN: We're on Windows and no dark style is set mocked_is_win.return_value = True MockSettings.return_value.value.return_value = True - + # WHEN: can_show_icon() is called result = get_application_stylesheet() From cd1b6715b6cd01e31cd760140d0b81e723aabed3 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Sep 2017 19:53:04 +0100 Subject: [PATCH 52/52] fix merge comments --- openlp/core/api/http/server.py | 8 ++++---- openlp/core/api/tab.py | 2 ++ openlp/core/common/settings.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index 916aedddd..2a2ec7292 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -89,7 +89,7 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): self.thread.start() Registry().register_function('download_website', self.first_time) Registry().register_function('get_website_version', self.website_version) - Registry().set_flag('website_version', '0001_01_01') + Registry().set_flag('website_version', '0.0') def bootstrap_post_set_up(self): """ @@ -126,7 +126,7 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): Import web site code if active """ self.application.process_events() - progress = Progress(self) + progress = DownloadProgressDialog(self) progress.forceShow() self.application.process_events() time.sleep(1) @@ -147,12 +147,12 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): Registry().set_flag('website_version', self.version) -class Progress(QtWidgets.QProgressDialog): +class DownloadProgressDialog(QtWidgets.QProgressDialog): """ Local class to handle download display based and supporting httputils:get_web_page """ def __init__(self, parent): - super(Progress, self).__init__(parent.main_window) + super(DownloadProgressDialog, self).__init__(parent.main_window) self.parent = parent self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowTitle(translate('RemotePlugin', 'Importing Website')) diff --git a/openlp/core/api/tab.py b/openlp/core/api/tab.py index 3ec8c4515..e10a68238 100644 --- a/openlp/core/api/tab.py +++ b/openlp/core/api/tab.py @@ -222,6 +222,8 @@ class ApiTab(SettingsTab): self.remote_url.setText('{url}'.format(url=http_url)) http_url_temp = http_url + 'stage' self.stage_url.setText('{url}'.format(url=http_url_temp)) + http_url_temp = http_url + 'chords' + self.chords_url.setText('{url}'.format(url=http_url_temp)) http_url_temp = http_url + 'main' self.live_url.setText('{url}'.format(url=http_url_temp)) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 477925280..87efd10d3 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -177,7 +177,7 @@ class Settings(QtCore.QSettings): 'images/background color': '#000000', 'media/players': 'system,webkit', 'media/override player': QtCore.Qt.Unchecked, - 'remotes/download version': '0000_00_00', + 'remotes/download version': '0.0', 'players/background color': '#000000', 'servicemanager/last directory': None, 'servicemanager/last file': None,