diff --git a/openlp/.version b/openlp/.version index 6b4950e3d..437459cd9 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.4 +2.5.0 diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 6f6addbbd..ad06f3629 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -129,21 +129,21 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) - show_splash = Settings().value('core/show splash') - if show_splash: + can_show_splash = Settings().value('core/show splash') + if can_show_splash: self.splash = SplashScreen() self.splash.show() # make sure Qt really display the splash screen self.processEvents() # Check if OpenLP has been upgrade and if a backup of data should be created - self.backup_on_upgrade(has_run_wizard) + self.backup_on_upgrade(has_run_wizard, can_show_splash) # start the main app window self.main_window = MainWindow() Registry().execute('bootstrap_initialise') Registry().execute('bootstrap_post_set_up') Registry().initialise = False self.main_window.show() - if show_splash: + if can_show_splash: # now kill the splashscreen self.splash.finish(self.main_window) log.debug('Splashscreen closed') @@ -177,6 +177,38 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): self.shared_memory.create(1) return False + def is_data_path_missing(self): + """ + Check if the data folder path exists. + """ + data_folder_path = 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) + if status == QtWidgets.QMessageBox.No: + # If answer was "No", return "True", it will shutdown OpenLP in def main + log.info('User requested termination') + return True + # If answer was "Yes", remove the custom data path thus resetting the default location. + Settings().remove('advanced/data path') + log.info('Database location has been reset to the default settings.') + return False + def hook_exception(self, exc_type, value, traceback): """ Add an exception hook so that any uncaught exceptions are displayed in this window rather than somewhere where @@ -192,13 +224,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): self.exception_form = ExceptionForm() self.exception_form.exception_text_edit.setPlainText(''.join(format_exception(exc_type, value, traceback))) self.set_normal_cursor() + is_splash_visible = False + if hasattr(self, 'splash') and self.splash.isVisible(): + is_splash_visible = True + self.splash.hide() self.exception_form.exec() + if is_splash_visible: + self.splash.show() - def backup_on_upgrade(self, has_run_wizard): + def backup_on_upgrade(self, has_run_wizard, can_show_splash): """ Check if OpenLP has been upgraded, and ask if a backup of data should be made :param has_run_wizard: OpenLP has been run before + :param can_show_splash: Should OpenLP show the splash screen """ data_version = Settings().value('core/application version') openlp_version = get_application_version()['version'] @@ -207,9 +246,11 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): Settings().setValue('core/application version', openlp_version) # If data_version is different from the current version ask if we should backup the data folder elif data_version != openlp_version: + if self.splash.isVisible(): + self.splash.hide() if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'), - translate('OpenLP', 'OpenLP has been upgraded, do you want to create ' - 'a backup of OpenLPs data folder?'), + translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n' + 'a backup of the old data folder?'), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: # Create copy of data folder @@ -223,12 +264,14 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): translate('OpenLP', 'Backup of the data folder failed!')) return message = translate('OpenLP', - 'A backup of the data folder has been created' - 'at {text}').format(text=data_folder_backup_path) + 'A backup of the data folder has been created at:\n\n' + '{text}').format(text=data_folder_backup_path) QtWidgets.QMessageBox.information(None, translate('OpenLP', 'Backup'), message) # Update the version in the settings Settings().setValue('core/application version', openlp_version) + if can_show_splash: + self.splash.show() def process_events(self): """ @@ -343,6 +386,7 @@ def main(args=None): application.setOrganizationName('OpenLP') application.setOrganizationDomain('openlp.org') application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) + application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True) if args and args.portable: application.setApplicationName('OpenLPPortable') Settings.setDefaultFormat(Settings.IniFormat) @@ -368,9 +412,13 @@ def main(args=None): Registry.create() Registry().register('application', application) application.setApplicationVersion(get_application_version()['version']) - # Instance check + # 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() + # If the custom data path is missing and the user wants to restore the data path, quit OpenLP. + if application.is_data_path_missing(): + application.shared_memory.detach() + sys.exit() # Remove/convert obsolete settings. Settings().remove_obsolete_settings() # First time checks in settings diff --git a/openlp/core/lib/webpagereader.py b/openlp/core/common/httputils.py similarity index 74% rename from openlp/core/lib/webpagereader.py rename to openlp/core/common/httputils.py index 52c98bbaf..b3cb50ef3 100644 --- a/openlp/core/lib/webpagereader.py +++ b/openlp/core/common/httputils.py @@ -22,7 +22,9 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ +import hashlib import logging +import os import socket import sys import time @@ -32,7 +34,7 @@ import urllib.request from http.client import HTTPException from random import randint -from openlp.core.common import Registry +from openlp.core.common import Registry, trace_error_handler log = logging.getLogger(__name__ + '.__init__') @@ -92,7 +94,7 @@ class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) -def _get_user_agent(): +def get_user_agent(): """ Return a user agent customised for the platform the user is on. """ @@ -122,7 +124,7 @@ def get_web_page(url, header=None, update_openlp=False): urllib.request.install_opener(opener) req = urllib.request.Request(url) if not header or header[0].lower() != 'user-agent': - user_agent = _get_user_agent() + user_agent = get_user_agent() req.add_header('User-Agent', user_agent) if header: req.add_header(header[0], header[1]) @@ -179,4 +181,75 @@ def get_web_page(url, header=None, update_openlp=False): return page +def get_url_file_size(url): + """ + Get the size of a file. + + :param url: The URL of the file we want to download. + """ + 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: + if retries > CONNECTION_RETRIES: + raise + else: + retries += 1 + time.sleep(0.1) + continue + + +def url_get_file(callback, url, f_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. + + :param callback: the class which needs to be updated + :param url: URL to download + :param 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 + 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: + trace_error_handler(log) + filename.close() + os.remove(f_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) + return True + __all__ = ['get_web_page'] diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 58e8e71f4..79069e514 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -235,8 +235,8 @@ class Settings(QtCore.QSettings): ('remotes/thumbnails', 'api/thumbnails', []), ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6. - ('shortcuts/offlineHelpItem', 'shortcuts/HelpItem', []), # Online and Offline help were combined in 2.6. - ('shortcuts/onlineHelpItem', 'shortcuts/HelpItem', []) # Online and Offline help were combined in 2.6. + ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. + ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []) # Online and Offline help were combined in 2.6. ] @staticmethod @@ -295,7 +295,7 @@ class Settings(QtCore.QSettings): 'shortcuts/fileSaveItem': [QtGui.QKeySequence(QtGui.QKeySequence.Save)], 'shortcuts/fileOpenItem': [QtGui.QKeySequence(QtGui.QKeySequence.Open)], 'shortcuts/goLive': [], - 'shortcuts/HelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)], + 'shortcuts/userManualItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)], 'shortcuts/importThemeItem': [], 'shortcuts/importBibleItem': [], 'shortcuts/listViewBiblesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)], diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 50aed88c1..104f8e5aa 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -112,6 +112,7 @@ class UiStrings(object): self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural') self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular') self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural') + self.NoResults = translate('OpenLP.Ui', 'No Search Results') self.OLP = translate('OpenLP.Ui', 'OpenLP') self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?') self.OpenService = translate('OpenLP.Ui', 'Open service.') @@ -137,6 +138,7 @@ class UiStrings(object): self.Settings = translate('OpenLP.Ui', 'Settings') self.SaveService = translate('OpenLP.Ui', 'Save Service') self.Service = translate('OpenLP.Ui', 'Service') + self.ShortResults = translate('OpenLP.Ui', 'Please type more text to use \'Search As You Type\'') self.Split = translate('OpenLP.Ui', 'Optional &Split') self.SplitToolTip = translate('OpenLP.Ui', 'Split a slide into two only if it does not fit on the screen as one slide.') diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index d00d85b54..49365600d 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -129,16 +129,16 @@ def build_icon(icon): location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. :return: The build icon. """ - button_icon = QtGui.QIcon() if isinstance(icon, QtGui.QIcon): - button_icon = icon - elif isinstance(icon, str): - if icon.startswith(':/'): - button_icon.addPixmap(QtGui.QPixmap(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off) - else: - button_icon.addPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon)), QtGui.QIcon.Normal, QtGui.QIcon.Off) + return icon + pix_map = None + button_icon = QtGui.QIcon() + if isinstance(icon, str): + pix_map = QtGui.QPixmap(icon) elif isinstance(icon, QtGui.QImage): - button_icon.addPixmap(QtGui.QPixmap.fromImage(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off) + pix_map = QtGui.QPixmap.fromImage(icon) + if pix_map: + button_icon.addPixmap(pix_map, QtGui.QIcon.Normal, QtGui.QIcon.Off) return button_icon @@ -310,30 +310,23 @@ def expand_tags(text): def create_separated_list(string_list): """ - Returns a string that represents a join of a list of strings with a localized separator. This function corresponds + Returns a string that represents a join of a list of strings with a localized separator. + Localized separation will be done via the translate() function by the translators. - to QLocale::createSeparatedList which was introduced in Qt 4.8 and implements the algorithm from - http://www.unicode.org/reports/tr35/#ListPatterns - - :param string_list: List of unicode strings + :param string_list: List of unicode strings + :return: Formatted string """ - if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'): - return QtCore.QLocale().createSeparatedList(string_list) - if not string_list: - return '' - elif len(string_list) == 1: - return string_list[0] - # TODO: Verify mocking of translate() test before conversion - elif len(string_list) == 2: - return translate('OpenLP.core.lib', '%s and %s', - 'Locale list separator: 2 items') % (string_list[0], string_list[1]) + list_length = len(string_list) + if list_length == 1: + list_to_string = string_list[0] + elif list_length == 2: + list_to_string = translate('OpenLP.core.lib', '{one} and {two}').format(one=string_list[0], two=string_list[1]) + elif list_length > 2: + list_to_string = translate('OpenLP.core.lib', '{first} and {last}').format(first=', '.join(string_list[:-1]), + last=string_list[-1]) else: - merged = translate('OpenLP.core.lib', '%s, and %s', - 'Locale list separator: end') % (string_list[-2], string_list[-1]) - for index in reversed(list(range(1, len(string_list) - 2))): - merged = translate('OpenLP.core.lib', '%s, %s', - 'Locale list separator: middle') % (string_list[index], merged) - return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged) + list_to_string = '' + return list_to_string from .exceptions import ValidationError diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 2edea93cf..25de144ad 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -266,7 +266,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): self.search_text_layout.setObjectName('search_text_layout') self.search_text_label = QtWidgets.QLabel(self.search_widget) self.search_text_label.setObjectName('search_text_label') - self.search_text_edit = SearchEdit(self.search_widget) + self.search_text_edit = SearchEdit(self.search_widget, self.settings_section) self.search_text_edit.setObjectName('search_text_edit') self.search_text_label.setBuddy(self.search_text_edit) self.search_text_layout.addRow(self.search_text_label, self.search_text_edit) @@ -397,8 +397,6 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): # Decide if we have to show the context menu or not. if item is None: return - if not item.flags() & QtCore.Qt.ItemIsSelectable: - return self.menu.exec(self.list_view.mapToGlobal(point)) def get_file_list(self): @@ -638,34 +636,6 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): """ return item - def check_search_result(self): - """ - Checks if the list_view is empty and adds a "No Search Results" item. - """ - if self.list_view.count(): - return - message = translate('OpenLP.MediaManagerItem', 'No Search Results') - item = QtWidgets.QListWidgetItem(message) - item.setFlags(QtCore.Qt.NoItemFlags) - font = QtGui.QFont() - font.setItalic(True) - item.setFont(font) - self.list_view.addItem(item) - - def check_search_result_search_while_typing_short(self): - """ - This is used in Bible "Search while typing" if the search is shorter than the min required len. - """ - if self.list_view.count(): - return - message = translate('OpenLP.MediaManagerItem', 'Search is too short to be used in: "Search while typing"') - item = QtWidgets.QListWidgetItem(message) - item.setFlags(QtCore.Qt.NoItemFlags) - font = QtGui.QFont() - font.setItalic(True) - item.setFont(font) - self.list_view.addItem(item) - def _get_id_of_item_to_generate(self, item, remote_item): """ Utility method to check items being submitted for slide generation. diff --git a/openlp/core/lib/searchedit.py b/openlp/core/lib/searchedit.py index 41e54957d..1fcc0e618 100644 --- a/openlp/core/lib/searchedit.py +++ b/openlp/core/lib/searchedit.py @@ -26,6 +26,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.lib import build_icon from openlp.core.lib.ui import create_widget_action +from openlp.core.common import Settings log = logging.getLogger(__name__) @@ -37,11 +38,12 @@ class SearchEdit(QtWidgets.QLineEdit): searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant) cleared = QtCore.pyqtSignal() - def __init__(self, parent): + def __init__(self, parent, settings_section): """ Constructor. """ - super(SearchEdit, self).__init__(parent) + super().__init__(parent) + self.settings_section = settings_section self._current_search_type = -1 self.clear_button = QtWidgets.QToolButton(self) self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png')) @@ -100,14 +102,10 @@ class SearchEdit(QtWidgets.QLineEdit): menu = self.menu_button.menu() for action in menu.actions(): if identifier == action.data(): - # setPlaceholderText has been implemented in Qt 4.7 and in at least PyQt 4.9 (I am not sure, if it was - # implemented in PyQt 4.8). - try: - self.setPlaceholderText(action.placeholder_text) - except AttributeError: - pass + self.setPlaceholderText(action.placeholder_text) self.menu_button.setDefaultAction(action) self._current_search_type = identifier + Settings().setValue('{section}/last search type'.format(section=self.settings_section), identifier) self.searchTypeChanged.emit(identifier) return True @@ -130,14 +128,10 @@ class SearchEdit(QtWidgets.QLineEdit): (2, ":/songs/authors.png", "Authors", "Search Authors...") """ menu = QtWidgets.QMenu(self) - first = None for identifier, icon, title, placeholder in items: action = create_widget_action( menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered) action.placeholder_text = placeholder - if first is None: - first = action - self._current_search_type = identifier if not hasattr(self, 'menu_button'): self.menu_button = QtWidgets.QToolButton(self) self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png')) @@ -146,7 +140,8 @@ class SearchEdit(QtWidgets.QLineEdit): self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }') self.menu_button.resize(QtCore.QSize(28, 18)) self.menu_button.setMenu(menu) - self.menu_button.setDefaultAction(first) + self.set_current_search_type( + Settings().value('{section}/last search type'.format(section=self.settings_section))) self.menu_button.show() self._update_style_sheet() diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py index 918e48e64..3f9034dcb 100644 --- a/openlp/core/ui/aboutdialog.py +++ b/openlp/core/ui/aboutdialog.py @@ -41,6 +41,7 @@ class UiAboutDialog(object): about_dialog.setObjectName('about_dialog') about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.about_dialog_layout = QtWidgets.QVBoxLayout(about_dialog) + self.about_dialog_layout.setContentsMargins(8, 8, 8, 8) self.about_dialog_layout.setObjectName('about_dialog_layout') self.logo_label = QtWidgets.QLabel(about_dialog) self.logo_label.setPixmap(QtGui.QPixmap(':/graphics/openlp-about-logo.png')) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index ca91e882a..fed712ed2 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -397,27 +397,6 @@ class AdvancedTab(SettingsTab): self.data_directory_cancel_button.hide() # Since data location can be changed, make sure the path is present. self.current_data_path = AppLocation.get_data_path() - if not os.path.exists(self.current_data_path): - log.error('Data path not found {path}'.format(path=self.current_data_path)) - answer = QtWidgets.QMessageBox.critical( - self, translate('OpenLP.AdvancedTab', 'Data Directory Error'), - translate('OpenLP.AdvancedTab', 'OpenLP data directory was not found\n\n{path}\n\n' - 'This data directory was previously changed from the OpenLP ' - 'default location. If the new location was on removable ' - 'media, that media needs to be made available.\n\n' - 'Click "No" to stop loading OpenLP. allowing you to fix the the problem.\n\n' - 'Click "Yes" to reset the data directory to the default ' - 'location.').format(path=self.current_data_path), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), - QtWidgets.QMessageBox.No) - if answer == QtWidgets.QMessageBox.No: - log.info('User requested termination') - self.main_window.clean_up() - sys.exit() - # Set data location to default. - settings.remove('advanced/data path') - self.current_data_path = AppLocation.get_data_path() - log.warning('User requested data path set to default {path}'.format(path=self.current_data_path)) self.data_directory_label.setText(os.path.abspath(self.current_data_path)) # Don't allow data directory move if running portable. if settings.value('advanced/is portable'): diff --git a/openlp/core/ui/exceptiondialog.py b/openlp/core/ui/exceptiondialog.py index f01cf5b26..36e039cee 100644 --- a/openlp/core/ui/exceptiondialog.py +++ b/openlp/core/ui/exceptiondialog.py @@ -38,7 +38,7 @@ class Ui_ExceptionDialog(object): Set up the UI. """ exception_dialog.setObjectName('exception_dialog') - exception_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + exception_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.exception_layout = QtWidgets.QVBoxLayout(exception_dialog) self.exception_layout.setObjectName('exception_layout') self.message_layout = QtWidgets.QHBoxLayout() diff --git a/openlp/core/ui/filerenamedialog.py b/openlp/core/ui/filerenamedialog.py index ede60bcfb..cc4574279 100644 --- a/openlp/core/ui/filerenamedialog.py +++ b/openlp/core/ui/filerenamedialog.py @@ -37,7 +37,7 @@ class Ui_FileRenameDialog(object): Set up the UI """ file_rename_dialog.setObjectName('file_rename_dialog') - file_rename_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + file_rename_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) file_rename_dialog.resize(300, 10) self.dialog_layout = QtWidgets.QGridLayout(file_rename_dialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 6ff19187f..73974bb14 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -22,7 +22,6 @@ """ This module contains the first time wizard. """ -import hashlib import logging import os import socket @@ -39,7 +38,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin translate, clean_button_text, trace_error_handler from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box -from openlp.core.lib.webpagereader import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT +from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT from .firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) @@ -394,54 +393,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.was_cancelled = True self.close() - def url_get_file(self, url, f_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. - - :param url: URL to download - :param f_path: Destination file - """ - block_count = 0 - block_size = 4096 - retries = 0 - 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 self.was_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) - if sha256: - hasher.update(data) - block_count += 1 - self._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: - trace_error_handler(log) - filename.close() - os.remove(f_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 self.was_cancelled: - os.remove(f_path) - return True - def _build_theme_screenshots(self): """ This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``. @@ -454,26 +405,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): if item: item.setIcon(build_icon(os.path.join(gettempdir(), 'openlp', screenshot))) - def _get_file_size(self, url): - """ - Get the size of a file. - - :param url: The URL of the file we want to download. - """ - 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: - if retries > CONNECTION_RETRIES: - raise - else: - retries += 1 - time.sleep(0.1) - continue - def _download_progress(self, count, block_size): """ Calculate and display the download progress. @@ -509,7 +440,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = self.songs_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename, sha256 = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.songs_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.songs_url, name=filename)) self.max_progress += size # Loop through the Bibles list and increase for each selected item iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -518,7 +449,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = iterator.value() if item.parent() and item.checkState(0) == QtCore.Qt.Checked: filename, sha256 = item.data(0, QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.bibles_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.bibles_url, name=filename)) self.max_progress += size iterator += 1 # Loop through the themes list and increase for each selected item @@ -527,7 +458,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = self.themes_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename, sha256 = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.themes_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.themes_url, name=filename)) self.max_progress += size except urllib.error.URLError: trace_error_handler(log) @@ -634,8 +565,8 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=filename), 0) self.previous_size = 0 destination = os.path.join(songs_destination, str(filename)) - if not self.url_get_file('{path}{name}'.format(path=self.songs_url, name=filename), - destination, sha256): + 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)) # Download Bibles bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -646,9 +577,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): # TODO: Tested at home self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 - if not self.url_get_file('{path}{name}'.format(path=self.bibles_url, name=bible), - os.path.join(bibles_destination, bible), - sha256): + if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), + os.path.join(bibles_destination, bible), + sha256): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 # Download themes @@ -659,9 +590,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): # TODO: Tested at home self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 - if not self.url_get_file('{path}{name}'.format(path=self.themes_url, name=theme), - os.path.join(themes_destination, theme), - sha256): + if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), + os.path.join(themes_destination, theme), + sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: file_list = '' diff --git a/openlp/core/ui/firsttimelanguagedialog.py b/openlp/core/ui/firsttimelanguagedialog.py index dd1389ba2..f32ec9076 100644 --- a/openlp/core/ui/firsttimelanguagedialog.py +++ b/openlp/core/ui/firsttimelanguagedialog.py @@ -38,7 +38,7 @@ class Ui_FirstTimeLanguageDialog(object): Set up the UI. """ language_dialog.setObjectName('language_dialog') - language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + language_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) language_dialog.resize(300, 50) self.dialog_layout = QtWidgets.QVBoxLayout(language_dialog) self.dialog_layout.setContentsMargins(8, 8, 8, 8) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 7b93c1680..63937ebb2 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -55,7 +55,7 @@ class UiFirstTimeWizard(object): :param first_time_wizard: The wizard form """ first_time_wizard.setObjectName('first_time_wizard') - first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + first_time_wizard.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) first_time_wizard.resize(550, 386) first_time_wizard.setModal(True) first_time_wizard.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage | diff --git a/openlp/core/ui/formattingtagdialog.py b/openlp/core/ui/formattingtagdialog.py index 19412921d..83ec18115 100644 --- a/openlp/core/ui/formattingtagdialog.py +++ b/openlp/core/ui/formattingtagdialog.py @@ -38,7 +38,7 @@ class Ui_FormattingTagDialog(object): Set up the UI """ formatting_tag_dialog.setObjectName('formatting_tag_dialog') - formatting_tag_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + formatting_tag_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) formatting_tag_dialog.resize(725, 548) self.list_data_grid_layout = QtWidgets.QVBoxLayout(formatting_tag_dialog) self.list_data_grid_layout.setContentsMargins(8, 8, 8, 8) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 463aad73f..629e55e0f 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -44,7 +44,7 @@ class GeneralTab(SettingsTab): self.logo_file = ':/graphics/openlp-splash-screen.png' self.logo_background_color = '#ffffff' self.screens = ScreenList() - self.icon_path = ':/icon/openlp-logo-16x16.png' + self.icon_path = ':/icon/openlp-logo.svg' general_translated = translate('OpenLP.GeneralTab', 'General') super(GeneralTab, self).__init__(parent, 'Core', general_translated) diff --git a/openlp/core/ui/lib/listwidgetwithdnd.py b/openlp/core/ui/lib/listwidgetwithdnd.py index 43ec741d0..de601fa13 100644 --- a/openlp/core/ui/lib/listwidgetwithdnd.py +++ b/openlp/core/ui/lib/listwidgetwithdnd.py @@ -26,7 +26,7 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import Registry +from openlp.core.common import Registry, UiStrings class ListWidgetWithDnD(QtWidgets.QListWidget): @@ -37,8 +37,9 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): """ Initialise the list widget """ - super(ListWidgetWithDnD, self).__init__(parent) + super().__init__(parent) self.mime_data_text = name + self.no_results_text = UiStrings().NoResults def activateDnD(self): """ @@ -48,6 +49,19 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) + def clear(self, search_while_typing=False): + """ + Re-implement clear, so that we can customise feedback when using 'Search as you type' + + :param search_while_typing: True if we want to display the customised message + :return: None + """ + if search_while_typing: + self.no_results_text = UiStrings().ShortResults + else: + self.no_results_text = UiStrings().NoResults + super().clear() + def mouseMoveEvent(self, event): """ Drag and drop event does not care what data is selected as the recipient will use events to request the data @@ -102,6 +116,24 @@ class ListWidgetWithDnD(QtWidgets.QListWidget): listing = os.listdir(local_file) for file in listing: files.append(os.path.join(local_file, file)) - Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) + Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text), + {'files': files, 'target': self.itemAt(event.pos())}) else: event.ignore() + + def paintEvent(self, event): + """ + Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. + + :param event: A QPaintEvent + :return: None + """ + super().paintEvent(event) + if not self.count(): + viewport = self.viewport() + painter = QtGui.QPainter(viewport) + font = QtGui.QFont() + font.setItalic(True) + painter.setFont(font) + painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()), + (QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text) diff --git a/openlp/core/ui/lib/mediadockmanager.py b/openlp/core/ui/lib/mediadockmanager.py index ad786b3a0..1a7676465 100644 --- a/openlp/core/ui/lib/mediadockmanager.py +++ b/openlp/core/ui/lib/mediadockmanager.py @@ -54,7 +54,7 @@ class MediaDockManager(object): match = True break if not match: - self.media_dock.addItem(media_item, visible_title['title']) + self.media_dock.addItem(media_item, media_item.plugin.icon, visible_title['title']) def remove_dock(self, media_item): """ diff --git a/openlp/core/ui/lib/treewidgetwithdnd.py b/openlp/core/ui/lib/treewidgetwithdnd.py index c49fc144e..f410e453a 100644 --- a/openlp/core/ui/lib/treewidgetwithdnd.py +++ b/openlp/core/ui/lib/treewidgetwithdnd.py @@ -26,7 +26,7 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import Registry +from openlp.core.common import Registry, is_win class TreeWidgetWithDnD(QtWidgets.QTreeWidget): @@ -108,6 +108,11 @@ class TreeWidgetWithDnD(QtWidgets.QTreeWidget): :param event: Handle of the event pint passed """ + # If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and + # the folder stays on top of the group creation box. This piece of code fixes this issue. + if is_win(): + self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + self.setWindowState(QtCore.Qt.WindowNoState) if event.mimeData().hasUrls(): event.setDropAction(QtCore.Qt.CopyAction) event.accept() diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 5f2321f48..06225a376 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -46,6 +46,7 @@ class WizardStrings(object): OSIS = 'OSIS' ZEF = 'Zefania' SWORD = 'Sword' + WordProject = 'WordProject' # These strings should need a good reason to be retranslated elsewhere. FinishedImport = translate('OpenLP.Ui', 'Finished import.') FormatLabel = translate('OpenLP.Ui', 'Format:') @@ -95,6 +96,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): super(OpenLPWizard, self).__init__(parent) self.plugin = plugin self.with_progress_page = add_progress_page + self.setFixedWidth(640) self.setObjectName(name) self.open_icon = build_icon(':/general/general_open.png') self.delete_icon = build_icon(':/general/general_delete.png') diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 54baddbcf..1a5a14fd9 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -55,21 +55,17 @@ from openlp.core.ui.lib.mediadockmanager import MediaDockManager log = logging.getLogger(__name__) MEDIA_MANAGER_STYLE = """ -QToolBox { - padding-bottom: 2px; -} -QToolBox::tab { +::tab#media_tool_box { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(button), stop: 1.0 palette(mid)); - border: 1px solid palette(mid); - border-radius: 3px; -} -QToolBox::tab:selected { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 palette(light), stop: 1.0 palette(button)); - border: 1px solid palette(mid); - font-weight: bold; + 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 = """ @@ -315,10 +311,9 @@ class Ui_MainWindow(object): elif is_macosx(): self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), '..', 'Resources', 'OpenLP.help') - self.on_help_item = create_action(main_window, 'HelpItem', - icon=':/system/system_help_contents.png', - can_shortcuts=True, - category=UiStrings().Help, triggers=self.on_help_clicked) + self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png', + can_shortcuts=True, category=UiStrings().Help, + triggers=self.on_help_clicked) self.web_site_item = create_action(main_window, 'webSiteItem', can_shortcuts=True, category=UiStrings().Help) # Shortcuts not connected to buttons or menu entries. self.search_shortcut_action = create_action(main_window, @@ -357,7 +352,7 @@ class Ui_MainWindow(object): add_actions(self.tools_menu, (self.tools_open_data_folder, None)) add_actions(self.tools_menu, (self.tools_first_time_wizard, None)) add_actions(self.tools_menu, [self.update_theme_images]) - add_actions(self.help_menu, (self.on_help_item, None, self.web_site_item, self.about_item)) + add_actions(self.help_menu, (self.user_manual_item, None, self.web_site_item, self.about_item)) add_actions(self.menu_bar, (self.file_menu.menuAction(), self.view_menu.menuAction(), self.tools_menu.menuAction(), self.settings_menu.menuAction(), self.help_menu.menuAction())) add_actions(self, [self.search_shortcut_action]) @@ -453,7 +448,7 @@ class Ui_MainWindow(object): 'from here.')) self.about_item.setText(translate('OpenLP.MainWindow', '&About')) self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP.')) - self.on_help_item.setText(translate('OpenLP.MainWindow', '&User Manual')) + self.user_manual_item.setText(translate('OpenLP.MainWindow', '&User Manual')) self.search_shortcut_action.setText(UiStrings().Search) self.search_shortcut_action.setToolTip( translate('OpenLP.MainWindow', 'Jump to the search box of the current active plugin.')) diff --git a/openlp/core/ui/plugindialog.py b/openlp/core/ui/plugindialog.py index ad461cd44..cd41d77a8 100644 --- a/openlp/core/ui/plugindialog.py +++ b/openlp/core/ui/plugindialog.py @@ -38,7 +38,7 @@ class Ui_PluginViewDialog(object): Set up the UI """ plugin_view_dialog.setObjectName('plugin_view_dialog') - plugin_view_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + plugin_view_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) plugin_view_dialog.setWindowModality(QtCore.Qt.ApplicationModal) self.plugin_layout = QtWidgets.QVBoxLayout(plugin_view_dialog) self.plugin_layout.setObjectName('plugin_layout') diff --git a/openlp/core/ui/printservicedialog.py b/openlp/core/ui/printservicedialog.py index c658d1496..f82bfecd4 100644 --- a/openlp/core/ui/printservicedialog.py +++ b/openlp/core/ui/printservicedialog.py @@ -50,7 +50,7 @@ class Ui_PrintServiceDialog(object): Set up the UI """ print_service_dialog.setObjectName('print_service_dialog') - print_service_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + print_service_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) print_service_dialog.resize(664, 594) self.main_layout = QtWidgets.QVBoxLayout(print_service_dialog) self.main_layout.setSpacing(0) diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index f4cf8a774..f15d5b550 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -47,7 +47,7 @@ class Ui_ProjectorEditForm(object): Create the interface layout. """ edit_projector_dialog.setObjectName('edit_projector_dialog') - edit_projector_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo-32x32.png')) + edit_projector_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) edit_projector_dialog.setMinimumWidth(400) edit_projector_dialog.setModal(True) # Define the basic layout diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index c2b1c2a1b..62b0a2158 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -243,7 +243,7 @@ class SourceSelectTabs(QtWidgets.QDialog): title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') self.setWindowTitle(title) self.setObjectName('source_select_tabs') - self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setWindowIcon(build_icon(':/icon/openlp-log.svg')) self.setModal(True) self.layout = QtWidgets.QVBoxLayout() self.layout.setObjectName('source_select_tabs_layout') @@ -395,7 +395,7 @@ class SourceSelectSingle(QtWidgets.QDialog): else: title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') self.setObjectName('source_select_single') - self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setWindowIcon(build_icon(':/icon/openlp-log.svg')) self.setModal(True) self.edit = edit diff --git a/openlp/core/ui/serviceitemeditdialog.py b/openlp/core/ui/serviceitemeditdialog.py index bbff73f3d..6dc45a847 100644 --- a/openlp/core/ui/serviceitemeditdialog.py +++ b/openlp/core/ui/serviceitemeditdialog.py @@ -38,7 +38,7 @@ class Ui_ServiceItemEditDialog(object): Set up the UI """ serviceItemEditDialog.setObjectName('serviceItemEditDialog') - serviceItemEditDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + serviceItemEditDialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.dialog_layout = QtWidgets.QGridLayout(serviceItemEditDialog) self.dialog_layout.setContentsMargins(8, 8, 8, 8) self.dialog_layout.setSpacing(8) diff --git a/openlp/core/ui/settingsdialog.py b/openlp/core/ui/settingsdialog.py index 3b6c4bc00..97c4c7233 100644 --- a/openlp/core/ui/settingsdialog.py +++ b/openlp/core/ui/settingsdialog.py @@ -38,7 +38,7 @@ class Ui_SettingsDialog(object): Set up the UI """ settings_dialog.setObjectName('settings_dialog') - settings_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + settings_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) settings_dialog.resize(800, 700) self.dialog_layout = QtWidgets.QGridLayout(settings_dialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/core/ui/shortcutlistdialog.py b/openlp/core/ui/shortcutlistdialog.py index ed1156025..4de2d4861 100644 --- a/openlp/core/ui/shortcutlistdialog.py +++ b/openlp/core/ui/shortcutlistdialog.py @@ -72,7 +72,7 @@ class Ui_ShortcutListDialog(object): Set up the UI """ shortcutListDialog.setObjectName('shortcutListDialog') - shortcutListDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + shortcutListDialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) shortcutListDialog.resize(500, 438) self.shortcut_list_layout = QtWidgets.QVBoxLayout(shortcutListDialog) self.shortcut_list_layout.setObjectName('shortcut_list_layout') diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 07386006c..5fede6911 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -726,8 +726,10 @@ class SlideController(DisplayController, RegistryProperties): # Reset the button self.play_slides_once.setChecked(False) self.play_slides_once.setIcon(build_icon(':/media/media_time.png')) + self.play_slides_once.setText(UiStrings().PlaySlidesToEnd) self.play_slides_loop.setChecked(False) self.play_slides_loop.setIcon(build_icon(':/media/media_time.png')) + self.play_slides_loop.setText(UiStrings().PlaySlidesInLoop) if item.is_text(): if (Settings().value(self.main_window.songs_settings_section + '/display songbar') and not self.song_menu.menu().isEmpty()): diff --git a/openlp/core/ui/starttimedialog.py b/openlp/core/ui/starttimedialog.py index 42287596c..e20d625d0 100644 --- a/openlp/core/ui/starttimedialog.py +++ b/openlp/core/ui/starttimedialog.py @@ -38,7 +38,7 @@ class Ui_StartTimeDialog(object): Set up the UI """ StartTimeDialog.setObjectName('StartTimeDialog') - StartTimeDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + StartTimeDialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) StartTimeDialog.resize(350, 10) self.dialog_layout = QtWidgets.QGridLayout(StartTimeDialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/core/ui/themelayoutdialog.py b/openlp/core/ui/themelayoutdialog.py index 5289e1cc2..119611f33 100644 --- a/openlp/core/ui/themelayoutdialog.py +++ b/openlp/core/ui/themelayoutdialog.py @@ -38,7 +38,7 @@ class Ui_ThemeLayoutDialog(object): Set up the UI """ themeLayoutDialog.setObjectName('themeLayoutDialogDialog') - themeLayoutDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + themeLayoutDialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.preview_layout = QtWidgets.QVBoxLayout(themeLayoutDialog) self.preview_layout.setObjectName('preview_layout') self.preview_area = QtWidgets.QWidget(themeLayoutDialog) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 427e9836b..7eac787d9 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -40,13 +40,13 @@ class Ui_ThemeWizard(object): Set up the UI """ theme_wizard.setObjectName('OpenLP.ThemeWizard') - theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + theme_wizard.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) theme_wizard.setModal(True) theme_wizard.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage | QtWidgets.QWizard.HaveCustomButton1) + theme_wizard.setFixedWidth(640) if is_macosx(): theme_wizard.setPixmap(QtWidgets.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - theme_wizard.resize(646, 400) else: theme_wizard.setWizardStyle(QtWidgets.QWizard.ModernStyle) self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) diff --git a/openlp/plugins/alerts/forms/alertdialog.py b/openlp/plugins/alerts/forms/alertdialog.py index 81af23ae1..83df7a09c 100644 --- a/openlp/plugins/alerts/forms/alertdialog.py +++ b/openlp/plugins/alerts/forms/alertdialog.py @@ -39,7 +39,7 @@ class Ui_AlertDialog(object): """ alert_dialog.setObjectName('alert_dialog') alert_dialog.resize(400, 300) - alert_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + alert_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.alert_dialog_layout = QtWidgets.QGridLayout(alert_dialog) self.alert_dialog_layout.setObjectName('alert_dialog_layout') self.alert_text_layout = QtWidgets.QFormLayout() diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index e1e062155..ae39d821b 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -125,6 +125,7 @@ class BibleImportForm(OpenLPWizard): self.csv_verses_button.clicked.connect(self.on_csv_verses_browse_button_clicked) self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked) self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked) + self.wordproject_browse_button.clicked.connect(self.on_wordproject_browse_button_clicked) self.web_update_button.clicked.connect(self.on_web_update_button_clicked) self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked) self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked) @@ -143,7 +144,7 @@ class BibleImportForm(OpenLPWizard): self.format_label = QtWidgets.QLabel(self.select_page) self.format_label.setObjectName('FormatLabel') self.format_combo_box = QtWidgets.QComboBox(self.select_page) - self.format_combo_box.addItems(['', '', '', '', '', '']) + self.format_combo_box.addItems(['', '', '', '', '', '', '']) self.format_combo_box.setObjectName('FormatComboBox') self.format_layout.addRow(self.format_label, self.format_combo_box) self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) @@ -161,6 +162,7 @@ class BibleImportForm(OpenLPWizard): self.osis_file_layout = QtWidgets.QHBoxLayout() self.osis_file_layout.setObjectName('OsisFileLayout') self.osis_file_edit = QtWidgets.QLineEdit(self.osis_widget) + self.osis_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.osis_file_edit.setObjectName('OsisFileEdit') self.osis_file_layout.addWidget(self.osis_file_edit) self.osis_browse_button = QtWidgets.QToolButton(self.osis_widget) @@ -180,6 +182,7 @@ class BibleImportForm(OpenLPWizard): self.csv_books_layout = QtWidgets.QHBoxLayout() self.csv_books_layout.setObjectName('CsvBooksLayout') self.csv_books_edit = QtWidgets.QLineEdit(self.csv_widget) + self.csv_books_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.csv_books_edit.setObjectName('CsvBooksEdit') self.csv_books_layout.addWidget(self.csv_books_edit) self.csv_books_button = QtWidgets.QToolButton(self.csv_widget) @@ -192,6 +195,7 @@ class BibleImportForm(OpenLPWizard): self.csv_verses_layout = QtWidgets.QHBoxLayout() self.csv_verses_layout.setObjectName('CsvVersesLayout') self.csv_verses_edit = QtWidgets.QLineEdit(self.csv_widget) + self.csv_verses_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.csv_verses_edit.setObjectName('CsvVersesEdit') self.csv_verses_layout.addWidget(self.csv_verses_edit) self.csv_verses_button = QtWidgets.QToolButton(self.csv_widget) @@ -211,6 +215,7 @@ class BibleImportForm(OpenLPWizard): self.open_song_file_layout = QtWidgets.QHBoxLayout() self.open_song_file_layout.setObjectName('OpenSongFileLayout') self.open_song_file_edit = QtWidgets.QLineEdit(self.open_song_widget) + self.open_song_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.open_song_file_edit.setObjectName('OpenSongFileEdit') self.open_song_file_layout.addWidget(self.open_song_file_edit) self.open_song_browse_button = QtWidgets.QToolButton(self.open_song_widget) @@ -300,61 +305,84 @@ class BibleImportForm(OpenLPWizard): self.sword_widget = QtWidgets.QWidget(self.select_page) self.sword_widget.setObjectName('SwordWidget') self.sword_layout = QtWidgets.QVBoxLayout(self.sword_widget) + self.sword_layout.setContentsMargins(0, 0, 0, 0) self.sword_layout.setObjectName('SwordLayout') self.sword_tab_widget = QtWidgets.QTabWidget(self.sword_widget) self.sword_tab_widget.setObjectName('SwordTabWidget') self.sword_folder_tab = QtWidgets.QWidget(self.sword_tab_widget) self.sword_folder_tab.setObjectName('SwordFolderTab') - self.sword_folder_tab_layout = QtWidgets.QGridLayout(self.sword_folder_tab) + self.sword_folder_tab_layout = QtWidgets.QFormLayout(self.sword_folder_tab) self.sword_folder_tab_layout.setObjectName('SwordTabFolderLayout') self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_folder_label.setObjectName('SwordSourceLabel') - self.sword_folder_tab_layout.addWidget(self.sword_folder_label, 0, 0) self.sword_folder_label.setObjectName('SwordFolderLabel') self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab) + self.sword_folder_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_folder_edit.setObjectName('SwordFolderEdit') self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab) self.sword_browse_button.setIcon(self.open_icon) self.sword_browse_button.setObjectName('SwordBrowseButton') - self.sword_folder_tab_layout.addWidget(self.sword_folder_edit, 0, 1) - self.sword_folder_tab_layout.addWidget(self.sword_browse_button, 0, 2) + self.sword_folder_layout = QtWidgets.QHBoxLayout() + self.sword_folder_layout.addWidget(self.sword_folder_edit) + self.sword_folder_layout.addWidget(self.sword_browse_button) + self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_layout) self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_bible_label.setObjectName('SwordBibleLabel') - self.sword_folder_tab_layout.addWidget(self.sword_bible_label, 1, 0) self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab) self.sword_bible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.sword_bible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) self.sword_bible_combo_box.setObjectName('SwordBibleComboBox') - self.sword_folder_tab_layout.addWidget(self.sword_bible_combo_box, 1, 1) + self.sword_folder_tab_layout.addRow(self.sword_bible_label, self.sword_bible_combo_box) self.sword_tab_widget.addTab(self.sword_folder_tab, '') self.sword_zip_tab = QtWidgets.QWidget(self.sword_tab_widget) self.sword_zip_tab.setObjectName('SwordZipTab') - self.sword_zip_layout = QtWidgets.QGridLayout(self.sword_zip_tab) + self.sword_zip_layout = QtWidgets.QFormLayout(self.sword_zip_tab) self.sword_zip_layout.setObjectName('SwordZipLayout') self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab) self.sword_zipfile_label.setObjectName('SwordZipFileLabel') self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab) + self.sword_zipfile_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_zipfile_edit.setObjectName('SwordZipFileEdit') self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab) self.sword_zipbrowse_button.setIcon(self.open_icon) self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton') + self.sword_zipfile_layout = QtWidgets.QHBoxLayout() + self.sword_zipfile_layout.addWidget(self.sword_zipfile_edit) + self.sword_zipfile_layout.addWidget(self.sword_zipbrowse_button) + self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_layout) self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_zipbible_label.setObjectName('SwordZipBibleLabel') self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab) self.sword_zipbible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.sword_zipbible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) self.sword_zipbible_combo_box.setObjectName('SwordZipBibleComboBox') - self.sword_zip_layout.addWidget(self.sword_zipfile_label, 0, 0) - self.sword_zip_layout.addWidget(self.sword_zipfile_edit, 0, 1) - self.sword_zip_layout.addWidget(self.sword_zipbrowse_button, 0, 2) - self.sword_zip_layout.addWidget(self.sword_zipbible_label, 1, 0) - self.sword_zip_layout.addWidget(self.sword_zipbible_combo_box, 1, 1) + self.sword_zip_layout.addRow(self.sword_zipbible_label, self.sword_zipbible_combo_box) self.sword_tab_widget.addTab(self.sword_zip_tab, '') self.sword_layout.addWidget(self.sword_tab_widget) self.sword_disabled_label = QtWidgets.QLabel(self.sword_widget) self.sword_disabled_label.setObjectName('SwordDisabledLabel') self.sword_layout.addWidget(self.sword_disabled_label) self.select_stack.addWidget(self.sword_widget) + self.wordproject_widget = QtWidgets.QWidget(self.select_page) + self.wordproject_widget.setObjectName('WordProjectWidget') + self.wordproject_layout = QtWidgets.QFormLayout(self.wordproject_widget) + self.wordproject_layout.setContentsMargins(0, 0, 0, 0) + self.wordproject_layout.setObjectName('WordProjectLayout') + self.wordproject_file_label = QtWidgets.QLabel(self.wordproject_widget) + self.wordproject_file_label.setObjectName('WordProjectFileLabel') + self.wordproject_file_layout = QtWidgets.QHBoxLayout() + self.wordproject_file_layout.setObjectName('WordProjectFileLayout') + self.wordproject_file_edit = QtWidgets.QLineEdit(self.wordproject_widget) + self.wordproject_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + self.wordproject_file_edit.setObjectName('WordProjectFileEdit') + self.wordproject_file_layout.addWidget(self.wordproject_file_edit) + self.wordproject_browse_button = QtWidgets.QToolButton(self.wordproject_widget) + self.wordproject_browse_button.setIcon(self.open_icon) + self.wordproject_browse_button.setObjectName('WordProjectBrowseButton') + self.wordproject_file_layout.addWidget(self.wordproject_browse_button) + self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout) + self.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer) + self.select_stack.addWidget(self.wordproject_widget) self.select_page_layout.addLayout(self.select_stack) self.addPage(self.select_page) # License Page @@ -400,6 +428,7 @@ class BibleImportForm(OpenLPWizard): self.format_combo_box.setItemText(BibleFormat.OSIS, WizardStrings.OSIS) self.format_combo_box.setItemText(BibleFormat.CSV, WizardStrings.CSV) self.format_combo_box.setItemText(BibleFormat.OpenSong, WizardStrings.OS) + self.format_combo_box.setItemText(BibleFormat.WordProject, WizardStrings.WordProject) self.format_combo_box.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm', 'Web Download')) self.format_combo_box.setItemText(BibleFormat.Zefania, WizardStrings.ZEF) @@ -410,6 +439,7 @@ class BibleImportForm(OpenLPWizard): self.open_song_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_source_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Location:')) self.zefania_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) + self.wordproject_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_update_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Click to download bible list')) self.web_update_button.setText(translate('BiblesPlugin.ImportWizardForm', 'Download bible list')) self.web_source_combo_box.setItemText(WebDownload.Crosswalk, translate('BiblesPlugin.ImportWizardForm', @@ -468,6 +498,7 @@ class BibleImportForm(OpenLPWizard): """ Validate the current page before moving on to the next page. """ + log.debug(self.size()) if self.currentPage() == self.welcome_page: return True elif self.currentPage() == self.select_page: @@ -504,6 +535,12 @@ class BibleImportForm(OpenLPWizard): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF) self.zefania_file_edit.setFocus() return False + elif self.field('source_format') == BibleFormat.WordProject: + if not self.field('wordproject_file'): + critical_error_message_box(UiStrings().NFSs, + WizardStrings.YouSpecifyFile % WizardStrings.WordProject) + self.wordproject_file_edit.setFocus() + return False elif self.field('source_format') == BibleFormat.WebDownload: # If count is 0 the bible list has not yet been downloaded if self.web_translation_combo_box.count() == 0: @@ -627,6 +664,14 @@ class BibleImportForm(OpenLPWizard): self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.ZEF, self.zefania_file_edit, 'last directory import') + def on_wordproject_browse_button_clicked(self): + """ + Show the file open dialog for the WordProject file. + """ + # TODO: Verify format() with variable template + self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.WordProject, self.wordproject_file_edit, + 'last directory import') + def on_web_update_button_clicked(self): """ Download list of bibles from Crosswalk, BibleServer and BibleGateway. @@ -707,6 +752,7 @@ class BibleImportForm(OpenLPWizard): self.select_page.registerField('csv_versefile', self.csv_verses_edit) self.select_page.registerField('opensong_file', self.open_song_file_edit) self.select_page.registerField('zefania_file', self.zefania_file_edit) + self.select_page.registerField('wordproject_file', self.wordproject_file_edit) self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box) self.select_page.registerField('sword_folder_path', self.sword_folder_edit) @@ -799,6 +845,10 @@ class BibleImportForm(OpenLPWizard): # Import a Zefania bible. importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, filename=self.field('zefania_file')) + elif bible_type == BibleFormat.WordProject: + # Import a WordProject bible. + importer = self.manager.import_bible(BibleFormat.WordProject, name=license_version, + filename=self.field('wordproject_file')) elif bible_type == BibleFormat.SWORD: # Import a SWORD bible. if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): diff --git a/openlp/plugins/bibles/forms/booknamedialog.py b/openlp/plugins/bibles/forms/booknamedialog.py index 6679dc1f3..18adde74b 100644 --- a/openlp/plugins/bibles/forms/booknamedialog.py +++ b/openlp/plugins/bibles/forms/booknamedialog.py @@ -30,7 +30,7 @@ from openlp.core.lib.ui import create_button_box class Ui_BookNameDialog(object): def setupUi(self, book_name_dialog): book_name_dialog.setObjectName('book_name_dialog') - book_name_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + book_name_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) book_name_dialog.resize(400, 271) self.book_name_layout = QtWidgets.QVBoxLayout(book_name_dialog) self.book_name_layout.setSpacing(8) diff --git a/openlp/plugins/bibles/forms/editbibledialog.py b/openlp/plugins/bibles/forms/editbibledialog.py index f1e833637..73ff81239 100644 --- a/openlp/plugins/bibles/forms/editbibledialog.py +++ b/openlp/plugins/bibles/forms/editbibledialog.py @@ -32,7 +32,7 @@ from openlp.plugins.bibles.lib.db import BiblesResourcesDB class Ui_EditBibleDialog(object): def setupUi(self, edit_bible_dialog): edit_bible_dialog.setObjectName('edit_bible_dialog') - edit_bible_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + edit_bible_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) edit_bible_dialog.resize(520, 400) edit_bible_dialog.setModal(True) self.dialog_layout = QtWidgets.QVBoxLayout(edit_bible_dialog) diff --git a/openlp/plugins/bibles/forms/languagedialog.py b/openlp/plugins/bibles/forms/languagedialog.py index ea5c291a0..554c7e35d 100644 --- a/openlp/plugins/bibles/forms/languagedialog.py +++ b/openlp/plugins/bibles/forms/languagedialog.py @@ -30,7 +30,7 @@ from openlp.core.lib.ui import create_button_box class Ui_LanguageDialog(object): def setupUi(self, language_dialog): language_dialog.setObjectName('language_dialog') - language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + language_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) language_dialog.resize(400, 165) self.language_layout = QtWidgets.QVBoxLayout(language_dialog) self.language_layout.setSpacing(8) diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index d41187d93..071ab0119 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -32,7 +32,7 @@ from bs4 import BeautifulSoup, NavigableString, Tag from openlp.core.common import Registry, RegistryProperties, translate from openlp.core.lib.ui import critical_error_message_box -from openlp.core.lib.webpagereader import get_web_page +from openlp.core.common.httputils import get_web_page from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book diff --git a/openlp/plugins/bibles/lib/importers/wordproject.py b/openlp/plugins/bibles/lib/importers/wordproject.py new file mode 100644 index 000000000..f48749fc6 --- /dev/null +++ b/openlp/plugins/bibles/lib/importers/wordproject.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 re +import logging +from codecs import open as copen +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +from bs4 import BeautifulSoup, Tag, NavigableString + +from openlp.plugins.bibles.lib.bibleimport import BibleImport + +BOOK_NUMBER_PATTERN = re.compile(r'\[(\d+)\]') +REPLACE_SPACES = re.compile(r'\s{2,}') + +log = logging.getLogger(__name__) + + +class WordProjectBible(BibleImport): + """ + `WordProject `_ Bible format importer class. + """ + def _cleanup(self): + """ + Clean up after ourselves + """ + self.tmp.cleanup() + + def _unzip_file(self): + """ + Unzip the file to a temporary directory + """ + self.tmp = TemporaryDirectory() + zip_file = ZipFile(os.path.abspath(self.filename)) + zip_file.extractall(self.tmp.name) + self.base_dir = os.path.join(self.tmp.name, os.path.splitext(os.path.basename(self.filename))[0]) + + def process_books(self): + """ + Extract and create the bible books from the parsed html + + :param bible_data: parsed xml + :return: None + """ + with copen(os.path.join(self.base_dir, 'index.htm'), encoding='utf-8', errors='ignore') as index_file: + page = index_file.read() + soup = BeautifulSoup(page, 'lxml') + bible_books = soup.find('div', 'textOptions').find_all('li') + book_count = len(bible_books) + for li_book in bible_books: + log.debug(li_book) + if self.stop_import_flag: + break + # Sometimes the structure is "[1] Genesis", and sometimes it's "[1] Genesis" + if isinstance(li_book.contents[0], NavigableString) and str(li_book.contents[0]).strip(): + book_string = str(li_book.contents[0]) + book_name = str(li_book.a.contents[0]) + elif li_book.a: + book_string, book_name = str(li_book.a.contents[0]).split(' ', 1) + book_link = li_book.a['href'] + book_id = int(BOOK_NUMBER_PATTERN.search(book_string).group(1)) + book_name = book_name.strip() + db_book = self.find_and_create_book(book_name, book_count, self.language_id, book_id) + self.process_chapters(db_book, book_id, book_link) + self.session.commit() + + def process_chapters(self, db_book, book_id, book_link): + """ + Extract the chapters, and do some initial processing of the verses + + :param book: An OpenLP bible database book object + :param chapters: parsed chapters + :return: None + """ + log.debug(book_link) + book_file = os.path.join(self.base_dir, os.path.normpath(book_link)) + with copen(book_file, encoding='utf-8', errors='ignore') as f: + page = f.read() + soup = BeautifulSoup(page, 'lxml') + header_div = soup.find('div', 'textHeader') + chapters_p = header_div.find('p') + if not chapters_p: + chapters_p = soup.p + log.debug(chapters_p) + for item in chapters_p.contents: + if self.stop_import_flag: + break + if isinstance(item, Tag) and item.name in ['a', 'span']: + chapter_number = int(item.string.strip()) + self.set_current_chapter(db_book.name, chapter_number) + self.process_verses(db_book, book_id, chapter_number) + + def process_verses(self, db_book, book_number, chapter_number): + """ + Get the verses for a particular book + """ + chapter_file_name = os.path.join(self.base_dir, '{:02d}'.format(book_number), '{}.htm'.format(chapter_number)) + with copen(chapter_file_name, encoding='utf-8', errors='ignore') as chapter_file: + page = chapter_file.read() + soup = BeautifulSoup(page, 'lxml') + text_body = soup.find('div', 'textBody') + if text_body: + verses_p = text_body.find('p') + else: + verses_p = soup.find_all('p')[2] + verse_number = 0 + verse_text = '' + for item in verses_p.contents: + if self.stop_import_flag: + break + if isinstance(item, Tag) and 'verse' in item.get('class', []): + if verse_number > 0: + self.process_verse(db_book, chapter_number, verse_number, verse_text.strip()) + verse_number = int(item.string.strip()) + verse_text = '' + elif isinstance(item, NavigableString): + verse_text += str(item) + elif isinstance(item, Tag) and item.name in ['span', 'a']: + verse_text += str(item.string) + else: + log.warning('Can\'t store %s', item) + self.process_verse(db_book, chapter_number, verse_number, verse_text.strip()) + + def process_verse(self, db_book, chapter_number, verse_number, verse_text): + """ + Process a verse element + :param book: A database Book object + :param chapter_number: The chapter number to add the verses to (int) + :param element: The verse element to process. (etree element type) + :param use_milestones: set to True to process a 'milestone' verse. Defaults to False + :return: None + """ + if verse_text: + log.debug('%s %s:%s %s', db_book.name, chapter_number, verse_number, verse_text.strip()) + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + + def do_import(self, bible_name=None): + """ + Loads a Bible from file. + """ + self.log_debug('Starting WordProject import from "{name}"'.format(name=self.filename)) + self._unzip_file() + self.language_id = self.get_language_id(None, bible_name=self.filename) + result = False + if self.language_id: + self.process_books() + result = True + self._cleanup() + return result diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 4ce1ace43..084e24270 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -31,6 +31,7 @@ from .importers.http import HTTPBible from .importers.opensong import OpenSongBible from .importers.osis import OSISBible from .importers.zefania import ZefaniaBible +from .importers.wordproject import WordProjectBible try: from .importers.sword import SwordBible except: @@ -50,6 +51,7 @@ class BibleFormat(object): WebDownload = 3 Zefania = 4 SWORD = 5 + WordProject = 6 @staticmethod def get_class(bible_format): @@ -70,6 +72,8 @@ class BibleFormat(object): return ZefaniaBible elif bible_format == BibleFormat.SWORD: return SwordBible + elif bible_format == BibleFormat.WordProject: + return WordProjectBible else: return None @@ -84,7 +88,8 @@ class BibleFormat(object): BibleFormat.OpenSong, BibleFormat.WebDownload, BibleFormat.Zefania, - BibleFormat.SWORD + BibleFormat.SWORD, + BibleFormat.WordProject ] @@ -131,7 +136,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): name = bible.get_name() # Remove corrupted files. if name is None: - bible.session.close() + bible.session.close_all() delete_file(os.path.join(self.path, filename)) continue log.debug('Bible Name: "{name}"'.format(name=name)) @@ -178,7 +183,7 @@ class BibleManager(OpenLPMixin, RegistryProperties): """ log.debug('BibleManager.delete_bible("{name}")'.format(name=name)) bible = self.db_cache[name] - bible.session.close() + bible.session.close_all() bible.session = None return delete_file(os.path.join(bible.path, bible.file)) diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index fc92b8f7a..c9b5c6b1e 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -75,21 +75,16 @@ class BibleMediaItem(MediaManagerItem): self.has_search = True self.search_results = {} self.second_search_results = {} - self.check_search_result() Registry().register_function('bibles_load_list', self.reload_bibles) def __check_second_bible(self, bible, second_bible): """ Check if the first item is a second bible item or not. """ - bitem = self.list_view.item(0) - if not bitem.flags() & QtCore.Qt.ItemIsSelectable: - # The item is the "No Search Results" item. - self.list_view.clear() + if not self.list_view.count(): self.display_results(bible, second_bible) return - else: - item_second_bible = self._decode_qt_object(bitem, 'second_bible') + item_second_bible = self._decode_qt_object(self.list_view.item(0), 'second_bible') if item_second_bible and second_bible or not item_second_bible and not second_bible: self.display_results(bible, second_bible) elif critical_error_message_box( @@ -199,7 +194,7 @@ class BibleMediaItem(MediaManagerItem): self.quick_search_label = QtWidgets.QLabel(self.quickTab) self.quick_search_label.setObjectName('quick_search_label') self.quickLayout.addWidget(self.quick_search_label, 0, 0, QtCore.Qt.AlignRight) - self.quick_search_edit = SearchEdit(self.quickTab) + self.quick_search_edit = SearchEdit(self.quickTab, self.settings_section) self.quick_search_edit.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed) self.quick_search_edit.setObjectName('quick_search_edit') self.quick_search_label.setBuddy(self.quick_search_edit) @@ -333,8 +328,8 @@ class BibleMediaItem(MediaManagerItem): translate('BiblesPlugin.MediaItem', 'Text Search'), translate('BiblesPlugin.MediaItem', 'Search Text...')) ]) - text = self.settings_section - self.quick_search_edit.set_current_search_type(Settings().value('{text}/last search type'.format(text=text))) + if Settings().value(self.settings_section + '/reset to combined quick search'): + self.quick_search_edit.set_current_search_type(BibleSearch.Combined) self.config_update() log.debug('bible manager initialise complete') @@ -430,6 +425,7 @@ class BibleMediaItem(MediaManagerItem): verse_count = self.plugin.manager.get_verse_count_by_book_ref_id(bible, book_ref_id, 1) if verse_count == 0: self.advancedSearchButton.setEnabled(False) + log.warning('Not enough chapters in %s', book_ref_id) critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.')) else: self.advancedSearchButton.setEnabled(True) @@ -444,15 +440,6 @@ class BibleMediaItem(MediaManagerItem): only updated when we are doing reference or combined search, in text search the completion list is removed. """ log.debug('update_auto_completer') - # Save the current search type to the configuration. If setting for automatically resetting the search type to - # Combined is enabled, use that otherwise use the currently selected search type. - # Note: This setting requires a restart to take effect. - if Settings().value(self.settings_section + '/reset to combined quick search'): - Settings().setValue('{section}/last search type'.format(section=self.settings_section), - BibleSearch.Combined) - else: - Settings().setValue('{section}/last search type'.format(section=self.settings_section), - self.quick_search_edit.current_search_type()) # Save the current bible to the configuration. Settings().setValue('{section}/quick bible'.format(section=self.settings_section), self.quickVersionComboBox.currentText()) @@ -551,14 +538,12 @@ class BibleMediaItem(MediaManagerItem): def on_clear_button_clicked(self): # Clear the list, then set the "No search Results" message, then clear the text field and give it focus. self.list_view.clear() - self.check_search_result() self.quick_search_edit.clear() self.quick_search_edit.setFocus() def on_advanced_clear_button_clicked(self): # The same as the on_clear_button_clicked, but gives focus to Book name field in "Select" (advanced). self.list_view.clear() - self.check_search_result() self.advanced_book_combo_box.setFocus() def on_lock_button_toggled(self, checked): @@ -692,7 +677,6 @@ class BibleMediaItem(MediaManagerItem): elif self.search_results: self.display_results(bible, second_bible) self.advancedSearchButton.setEnabled(True) - self.check_search_result() self.application.set_normal_cursor() def on_quick_reference_search(self): @@ -886,7 +870,6 @@ class BibleMediaItem(MediaManagerItem): elif self.search_results: self.display_results(bible, second_bible) self.quickSearchButton.setEnabled(True) - self.check_search_result() self.application.set_normal_cursor() def on_quick_search_while_typing(self): @@ -917,7 +900,6 @@ class BibleMediaItem(MediaManagerItem): self.__check_second_bible(bible, second_bible) elif self.search_results: self.display_results(bible, second_bible) - self.check_search_result() self.application.set_normal_cursor() def on_search_text_edit_changed(self): @@ -956,17 +938,13 @@ class BibleMediaItem(MediaManagerItem): if len(text) == 0: if not self.quickLockButton.isChecked(): self.list_view.clear() - self.check_search_result() else: if limit == 3 and (len(text) < limit or len(count_space_digit_reference) == 0): if not self.quickLockButton.isChecked(): self.list_view.clear() - self.check_search_result() - elif (limit == 8 and (len(text) < limit or len(count_spaces_two_chars_text) == 0 or - len(count_two_chars_text) < 2)): + elif limit == 8 and (len(text) < limit or len(count_two_chars_text) < 2): if not self.quickLockButton.isChecked(): - self.list_view.clear() - self.check_search_result_search_while_typing_short() + self.list_view.clear(search_while_typing=True) else: """ Start search if no chars are entered or deleted for 0.2 s diff --git a/openlp/plugins/custom/forms/editcustomdialog.py b/openlp/plugins/custom/forms/editcustomdialog.py index b686e5522..d25ff337b 100644 --- a/openlp/plugins/custom/forms/editcustomdialog.py +++ b/openlp/plugins/custom/forms/editcustomdialog.py @@ -34,7 +34,7 @@ class Ui_CustomEditDialog(object): :param custom_edit_dialog: The Dialog """ custom_edit_dialog.setObjectName('custom_edit_dialog') - custom_edit_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + custom_edit_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) custom_edit_dialog.resize(450, 350) self.dialog_layout = QtWidgets.QVBoxLayout(custom_edit_dialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/plugins/custom/forms/editcustomslidedialog.py b/openlp/plugins/custom/forms/editcustomslidedialog.py index 5f46c6f28..d08679eee 100644 --- a/openlp/plugins/custom/forms/editcustomslidedialog.py +++ b/openlp/plugins/custom/forms/editcustomslidedialog.py @@ -31,7 +31,7 @@ from openlp.core.ui.lib import SpellTextEdit class Ui_CustomSlideEditDialog(object): def setupUi(self, custom_slide_edit_dialog): custom_slide_edit_dialog.setObjectName('custom_slide_edit_dialog') - custom_slide_edit_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + custom_slide_edit_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) custom_slide_edit_dialog.resize(350, 300) self.dialog_layout = QtWidgets.QVBoxLayout(custom_slide_edit_dialog) self.slide_text_edit = SpellTextEdit(self) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 2b999da19..eacc5a8f5 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -105,8 +105,6 @@ class CustomMediaItem(MediaManagerItem): [(CustomSearch.Titles, ':/songs/song_search_title.png', translate('SongsPlugin.MediaItem', 'Titles'), translate('SongsPlugin.MediaItem', 'Search Titles...')), (CustomSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes)]) - text = '{section}/last search type'.format(section=self.settings_section) - self.search_text_edit.set_current_search_type(Settings().value(text)) self.load_list(self.plugin.db_manager.get_all_objects(CustomSlide, order_by_ref=CustomSlide.title)) self.config_update() @@ -131,7 +129,6 @@ class CustomMediaItem(MediaManagerItem): # Called to redisplay the custom list screen edith from a search # or from the exit of the Custom edit dialog. If remote editing is # active trigger it and clean up so it will not update again. - self.check_search_result() def on_new_click(self): """ @@ -250,9 +247,6 @@ class CustomMediaItem(MediaManagerItem): """ Search the plugin database """ - # Save the current search type to the configuration. - Settings().setValue('{section}/last search type'.format(section=self.settings_section), - self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_type = self.search_text_edit.current_search_type() search_keywords = '%{search}%'.format(search=self.whitespace.sub(' ', self.search_text_edit.displayText())) @@ -268,7 +262,6 @@ class CustomMediaItem(MediaManagerItem): CustomSlide.theme_name.like(search_keywords), order_by_ref=CustomSlide.title) self.load_list(search_results) - self.check_search_result() def on_search_text_edit_changed(self, text): """ diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index dc196fb59..00fccb657 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -150,7 +150,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): triggers=self.on_replace_click) if 'webkit' not in get_media_players()[0]: self.replace_action.setDisabled(True) - self.replace_action_context.setDisabled(True) + if hasattr(self, 'replace_action_context'): + self.replace_action_context.setDisabled(True) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.on_reset_click) self.media_widget = QtWidgets.QWidget(self) diff --git a/openlp/plugins/songs/forms/authorsdialog.py b/openlp/plugins/songs/forms/authorsdialog.py index 666baffec..7b2995480 100644 --- a/openlp/plugins/songs/forms/authorsdialog.py +++ b/openlp/plugins/songs/forms/authorsdialog.py @@ -36,7 +36,7 @@ class Ui_AuthorsDialog(object): Set up the UI for the dialog. """ authors_dialog.setObjectName('authors_dialog') - authors_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + authors_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) authors_dialog.resize(300, 10) authors_dialog.setModal(True) self.dialog_layout = QtWidgets.QVBoxLayout(authors_dialog) diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py index 46e37c1cd..b23fe1189 100644 --- a/openlp/plugins/songs/forms/editsongdialog.py +++ b/openlp/plugins/songs/forms/editsongdialog.py @@ -36,7 +36,7 @@ class Ui_EditSongDialog(object): """ def setupUi(self, edit_song_dialog): edit_song_dialog.setObjectName('edit_song_dialog') - edit_song_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + edit_song_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) edit_song_dialog.resize(900, 600) edit_song_dialog.setModal(True) self.dialog_layout = QtWidgets.QVBoxLayout(edit_song_dialog) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index a17c9fb5f..271dadce7 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -118,13 +118,13 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): objects = self.manager.get_all_objects(cls) objects.sort(key=get_key) combo.clear() - combo.addItem('') for obj in objects: row = combo.count() combo.addItem(obj.name) cache.append(obj.name) combo.setItemData(row, obj.id) set_case_insensitive_completer(cache, combo) + combo.setEditText('') def _add_author_to_list(self, author, author_type): """ @@ -360,7 +360,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): authors = self.manager.get_all_objects(Author) authors.sort(key=get_author_key) self.authors_combo_box.clear() - self.authors_combo_box.addItem('') self.authors = [] for author in authors: row = self.authors_combo_box.count() @@ -368,6 +367,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.authors_combo_box.setItemData(row, author.id) self.authors.append(author.display_name) set_case_insensitive_completer(self.authors, self.authors_combo_box) + self.authors_combo_box.setEditText('') # Types self.author_types_combo_box.clear() @@ -398,11 +398,11 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): return get_natural_key(theme) self.theme_combo_box.clear() - self.theme_combo_box.addItem('') self.themes = theme_list self.themes.sort(key=get_theme_key) self.theme_combo_box.addItems(theme_list) set_case_insensitive_completer(self.themes, self.theme_combo_box) + self.theme_combo_box.setEditText('') def load_media_files(self): """ @@ -442,7 +442,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.load_songbooks() self.load_media_files() self.theme_combo_box.setEditText('') - self.theme_combo_box.setCurrentIndex(0) # it's a new song to preview is not possible self.preview_button.setVisible(False) @@ -591,7 +590,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.manager.save_object(author) self._add_author_to_list(author, author_type) self.load_authors() - self.authors_combo_box.setCurrentIndex(0) + self.authors_combo_box.setEditText('') else: return elif item > 0: @@ -602,7 +601,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.')) else: self._add_author_to_list(author, author_type) - self.authors_combo_box.setCurrentIndex(0) + self.authors_combo_box.setEditText('') else: QtWidgets.QMessageBox.warning( self, UiStrings().NISs, @@ -666,7 +665,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_item.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_item) self.load_topics() - self.topics_combo_box.setCurrentIndex(0) + self.topics_combo_box.setEditText('') else: return elif item > 0: @@ -679,7 +678,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_item = QtWidgets.QListWidgetItem(str(topic.name)) topic_item.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_item) - self.topics_combo_box.setCurrentIndex(0) + self.topics_combo_box.setEditText('') else: QtWidgets.QMessageBox.warning( self, UiStrings().NISs, @@ -709,7 +708,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.manager.save_object(songbook) self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.load_songbooks() - self.songbooks_combo_box.setCurrentIndex(0) + self.songbooks_combo_box.setEditText('') self.songbook_entry_edit.clear() else: return @@ -721,7 +720,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): message=translate('SongsPlugin.EditSongForm', 'This Songbook is already in the list.')) else: self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) - self.songbooks_combo_box.setCurrentIndex(0) + self.songbooks_combo_box.setEditText('') self.songbook_entry_edit.clear() else: QtWidgets.QMessageBox.warning( diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py index 6819f3ca6..13dce97fb 100644 --- a/openlp/plugins/songs/forms/editversedialog.py +++ b/openlp/plugins/songs/forms/editversedialog.py @@ -31,7 +31,7 @@ from openlp.plugins.songs.lib import VerseType class Ui_EditVerseDialog(object): def setupUi(self, edit_verse_dialog): edit_verse_dialog.setObjectName('edit_verse_dialog') - edit_verse_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + edit_verse_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) edit_verse_dialog.resize(400, 400) edit_verse_dialog.setModal(True) self.dialog_layout = QtWidgets.QVBoxLayout(edit_verse_dialog) diff --git a/openlp/plugins/songs/forms/mediafilesdialog.py b/openlp/plugins/songs/forms/mediafilesdialog.py index 4309eb91f..2b9bcceab 100644 --- a/openlp/plugins/songs/forms/mediafilesdialog.py +++ b/openlp/plugins/songs/forms/mediafilesdialog.py @@ -35,7 +35,7 @@ class Ui_MediaFilesDialog(object): Set up the user interface. """ media_files_dialog.setObjectName('media_files_dialog') - media_files_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + media_files_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) media_files_dialog.setWindowModality(QtCore.Qt.ApplicationModal) media_files_dialog.resize(400, 300) media_files_dialog.setModal(True) diff --git a/openlp/plugins/songs/forms/songbookdialog.py b/openlp/plugins/songs/forms/songbookdialog.py index 7e1c04d20..10ed154b4 100644 --- a/openlp/plugins/songs/forms/songbookdialog.py +++ b/openlp/plugins/songs/forms/songbookdialog.py @@ -35,7 +35,7 @@ class Ui_SongBookDialog(object): Set up the user interface. """ song_book_dialog.setObjectName('song_book_dialog') - song_book_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + song_book_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) song_book_dialog.resize(300, 10) self.dialog_layout = QtWidgets.QVBoxLayout(song_book_dialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 27f317963..b0d044b2d 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -51,7 +51,7 @@ class SongExportForm(OpenLPWizard): :param parent: The QWidget-derived parent of the wizard. :param plugin: The songs plugin. """ - super(SongExportForm, self).__init__(parent, plugin, 'song_export_wizard', ':/wizards/wizard_exportsong.bmp') + super(SongExportForm, self).__init__(parent, plugin, 'song_export_wizard', ':/wizards/wizard_song.bmp') self.stop_export_flag = False Registry().register_function('openlp_stop_wizard', self.stop_export) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 2539973da..b25021ca9 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -52,7 +52,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties): :param parent: The QWidget-derived parent of the wizard. :param plugin: The songs plugin. """ - super(SongImportForm, self).__init__(parent, plugin, 'songImportWizard', ':/wizards/wizard_importsong.bmp') + super(SongImportForm, self).__init__(parent, plugin, 'songImportWizard', ':/wizards/wizard_song.bmp') self.clipboard = self.main_window.clipboard def setupUi(self, image): diff --git a/openlp/plugins/songs/forms/songmaintenancedialog.py b/openlp/plugins/songs/forms/songmaintenancedialog.py index cd224267b..c0c532688 100644 --- a/openlp/plugins/songs/forms/songmaintenancedialog.py +++ b/openlp/plugins/songs/forms/songmaintenancedialog.py @@ -37,7 +37,7 @@ class Ui_SongMaintenanceDialog(object): Set up the user interface for the song maintenance dialog """ song_maintenance_dialog.setObjectName('song_maintenance_dialog') - song_maintenance_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + song_maintenance_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) song_maintenance_dialog.setWindowModality(QtCore.Qt.ApplicationModal) song_maintenance_dialog.resize(10, 350) self.dialog_layout = QtWidgets.QGridLayout(song_maintenance_dialog) diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index c8873f028..2e5814626 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -182,7 +182,7 @@ class SongReviewWidget(QtWidgets.QWidget): self.song_vertical_layout.addWidget(self.song_group_box) self.song_remove_button = QtWidgets.QPushButton(self) self.song_remove_button.setObjectName('song_remove_button') - self.song_remove_button.setIcon(build_icon(':/songs/song_delete.png')) + self.song_remove_button.setIcon(build_icon(':/general/general_delete.png')) self.song_remove_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self.song_vertical_layout.addWidget(self.song_remove_button, alignment=QtCore.Qt.AlignHCenter) diff --git a/openlp/plugins/songs/forms/topicsdialog.py b/openlp/plugins/songs/forms/topicsdialog.py index 1ed3ac61b..90c19238d 100644 --- a/openlp/plugins/songs/forms/topicsdialog.py +++ b/openlp/plugins/songs/forms/topicsdialog.py @@ -35,7 +35,7 @@ class Ui_TopicsDialog(object): Set up the user interface for the topics dialog. """ topics_dialog.setObjectName('topics_dialog') - topics_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + topics_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) topics_dialog.resize(300, 10) self.dialog_layout = QtWidgets.QVBoxLayout(topics_dialog) self.dialog_layout.setObjectName('dialog_layout') diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 63120c966..3e905448b 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -166,14 +166,9 @@ class SongMediaItem(MediaManagerItem): translate('SongsPlugin.MediaItem', 'CCLI number'), translate('SongsPlugin.MediaItem', 'Search CCLI number...')) ]) - self.search_text_edit.set_current_search_type( - Settings().value('{section}/last search type'.format(section=self.settings_section))) self.config_update() def on_search_text_button_clicked(self): - # Save the current search type to the configuration. - Settings().setValue('{section}/last search type'.format(section=self.settings_section), - self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_keywords = str(self.search_text_edit.displayText()) search_type = self.search_text_edit.current_search_type() @@ -232,7 +227,6 @@ class SongMediaItem(MediaManagerItem): search_results = self.plugin.manager.get_all_objects( Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != '')) self.display_results_cclinumber(search_results) - self.check_search_result() def search_entire(self, search_keywords): search_string = '%{text}%'.format(text=clean_string(search_keywords)) diff --git a/openlp/plugins/songusage/forms/songusagedeletedialog.py b/openlp/plugins/songusage/forms/songusagedeletedialog.py index 40fabe3f7..6f3f72651 100644 --- a/openlp/plugins/songusage/forms/songusagedeletedialog.py +++ b/openlp/plugins/songusage/forms/songusagedeletedialog.py @@ -38,7 +38,7 @@ class Ui_SongUsageDeleteDialog(object): :param song_usage_delete_dialog: """ song_usage_delete_dialog.setObjectName('song_usage_delete_dialog') - song_usage_delete_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + song_usage_delete_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) song_usage_delete_dialog.resize(291, 243) self.vertical_layout = QtWidgets.QVBoxLayout(song_usage_delete_dialog) self.vertical_layout.setSpacing(8) diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index b665e74c2..e9e4a6bbe 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -38,7 +38,7 @@ class Ui_SongUsageDetailDialog(object): :param song_usage_detail_dialog: """ song_usage_detail_dialog.setObjectName('song_usage_detail_dialog') - song_usage_detail_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + song_usage_detail_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) song_usage_detail_dialog.resize(609, 413) self.vertical_layout = QtWidgets.QVBoxLayout(song_usage_detail_dialog) self.vertical_layout.setSpacing(8) diff --git a/resources/images/about-new.bmp b/resources/images/about-new.bmp deleted file mode 100644 index b523f6b60..000000000 Binary files a/resources/images/about-new.bmp and /dev/null differ diff --git a/resources/images/bibles_search_clear.png b/resources/images/bibles_search_clear.png index 551ce7c96..19a1665ba 100644 Binary files a/resources/images/bibles_search_clear.png and b/resources/images/bibles_search_clear.png differ diff --git a/resources/images/bibles_search_combined.png b/resources/images/bibles_search_combined.png index 161b3fcd6..8fc16c886 100644 Binary files a/resources/images/bibles_search_combined.png and b/resources/images/bibles_search_combined.png differ diff --git a/resources/images/bibles_upgrade_alert.png b/resources/images/bibles_upgrade_alert.png deleted file mode 100644 index 331aa2687..000000000 Binary files a/resources/images/bibles_upgrade_alert.png and /dev/null differ diff --git a/resources/images/custom_delete.png b/resources/images/custom_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/custom_delete.png and /dev/null differ diff --git a/resources/images/custom_edit.png b/resources/images/custom_edit.png deleted file mode 100644 index d79aa5151..000000000 Binary files a/resources/images/custom_edit.png and /dev/null differ diff --git a/resources/images/custom_new.png b/resources/images/custom_new.png deleted file mode 100644 index 8431237bd..000000000 Binary files a/resources/images/custom_new.png and /dev/null differ diff --git a/resources/images/general_search_clear.png b/resources/images/general_search_clear.png deleted file mode 100644 index 6c4b83b7a..000000000 Binary files a/resources/images/general_search_clear.png and /dev/null differ diff --git a/resources/images/image_clapperboard.png b/resources/images/image_clapperboard.png deleted file mode 100644 index e46056914..000000000 Binary files a/resources/images/image_clapperboard.png and /dev/null differ diff --git a/resources/images/image_delete.png b/resources/images/image_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/image_delete.png and /dev/null differ diff --git a/resources/images/image_load.png b/resources/images/image_load.png deleted file mode 100644 index eb211e233..000000000 Binary files a/resources/images/image_load.png and /dev/null differ diff --git a/resources/images/import_load.png b/resources/images/import_load.png deleted file mode 100644 index eb211e233..000000000 Binary files a/resources/images/import_load.png and /dev/null differ diff --git a/resources/images/import_move_to_list.png b/resources/images/import_move_to_list.png deleted file mode 100644 index 5c0005856..000000000 Binary files a/resources/images/import_move_to_list.png and /dev/null differ diff --git a/resources/images/import_remove.png b/resources/images/import_remove.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/import_remove.png and /dev/null differ diff --git a/resources/images/import_selectall.png b/resources/images/import_selectall.png deleted file mode 100644 index 0f0d9f152..000000000 Binary files a/resources/images/import_selectall.png and /dev/null differ diff --git a/resources/images/messagebox_critical.png b/resources/images/messagebox_critical.png deleted file mode 100644 index 3fb497a19..000000000 Binary files a/resources/images/messagebox_critical.png and /dev/null differ diff --git a/resources/images/messagebox_info.png b/resources/images/messagebox_info.png deleted file mode 100644 index 4e3a257f3..000000000 Binary files a/resources/images/messagebox_info.png and /dev/null differ diff --git a/resources/images/messagebox_warning.png b/resources/images/messagebox_warning.png deleted file mode 100644 index 331aa2687..000000000 Binary files a/resources/images/messagebox_warning.png and /dev/null differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 867e4dd7a..ebff58a0a 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -23,8 +23,7 @@ topic_maintenance.png song_author_edit.png song_book_edit.png - song_delete.png - + image_group.png image_new_group.png @@ -33,7 +32,6 @@ bibles_search_combined.png bibles_search_text.png bibles_search_reference.png - bibles_upgrade_alert.png bibles_search_clear.png bibles_search_unlock.png bibles_search_lock.png @@ -71,7 +69,6 @@ general_back.png - slide_close.png slide_next.png slide_blank.png slide_desktop.png @@ -83,12 +80,6 @@ media_playback_next.png - openlp-logo-16x16.png - openlp-logo-32x32.png - openlp-logo-48x48.png - openlp-logo-64x64.png - openlp-logo-128x128.png - openlp-logo-256x256.png openlp-logo.svg @@ -97,24 +88,18 @@ openlp-about-logo.png - import_selectall.png - import_move_to_list.png - import_remove.png - import_load.png - + export_load.png openlp-osx-wizard.png - wizard_exportsong.bmp - wizard_importsong.bmp + wizard_song.bmp wizard_importbible.bmp wizard_firsttime.bmp wizard_createtheme.bmp wizard_duplicateremoval.bmp - wizard_createprojector.png - + service_collapse_all.png service_expand_all.png @@ -155,10 +140,7 @@ multimedia-player.png - messagebox_critical.png - messagebox_info.png - messagebox_warning.png - + network_server.png network_ssl.png @@ -170,10 +152,8 @@ tools_add.png - tools_alert.png - + - theme_delete.png theme_new.png theme_edit.png @@ -201,12 +181,10 @@ projector_power_on_tiled.png projector_show.png projector_show_tiled.png - projector_spacer.png projector_warmup.png - projector_view.png - + android_app_qr.png ios_app_qr.png - + \ No newline at end of file diff --git a/resources/images/openlp-about-logo.png b/resources/images/openlp-about-logo.png index a20534362..b2b141a17 100644 Binary files a/resources/images/openlp-about-logo.png and b/resources/images/openlp-about-logo.png differ diff --git a/resources/images/openlp-about-logo.svg b/resources/images/openlp-about-logo.svg deleted file mode 100644 index d4f9c2dbe..000000000 --- a/resources/images/openlp-about-logo.svg +++ /dev/null @@ -1,323 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/images/openlp-default-dualdisplay.svg b/resources/images/openlp-default-dualdisplay.svg deleted file mode 100644 index 40ebedeac..000000000 --- a/resources/images/openlp-default-dualdisplay.svg +++ /dev/null @@ -1,489 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/images/openlp-logo-256x256.png b/resources/images/openlp-logo-256x256.png deleted file mode 100644 index 0fcf37a7c..000000000 Binary files a/resources/images/openlp-logo-256x256.png and /dev/null differ diff --git a/resources/images/openlp-logo-420x420.png b/resources/images/openlp-logo-420x420.png deleted file mode 100644 index 81b893b9a..000000000 Binary files a/resources/images/openlp-logo-420x420.png and /dev/null differ diff --git a/resources/images/openlp-logo.svg b/resources/images/openlp-logo.svg index c5e7985e1..764ba8563 100644 --- a/resources/images/openlp-logo.svg +++ b/resources/images/openlp-logo.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - image/svg+xml - - - - - - - - - - - - - - - - - - + inkscape:zoom="1" + inkscape:cx="151.21623" + inkscape:cy="240.04907" + inkscape:window-x="1920" + inkscape:window-y="312" + inkscape:window-maximized="1" + inkscape:current-layer="svg5740" + fit-margin-left="0" + fit-margin-top="0" + fit-margin-right="0" + fit-margin-bottom="0" + showguides="true" + inkscape:guide-bbox="true"> \ No newline at end of file diff --git a/resources/images/openlp-splash-screen.png b/resources/images/openlp-splash-screen.png index 618e47c6e..09785488a 100644 Binary files a/resources/images/openlp-splash-screen.png and b/resources/images/openlp-splash-screen.png differ diff --git a/resources/images/openlp-splash-screen.svg b/resources/images/openlp-splash-screen.svg deleted file mode 100644 index b45f2015c..000000000 --- a/resources/images/openlp-splash-screen.svg +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/images/openlp.org.ico b/resources/images/openlp.org.ico deleted file mode 100644 index f11e76809..000000000 Binary files a/resources/images/openlp.org.ico and /dev/null differ diff --git a/resources/images/presentation_delete.png b/resources/images/presentation_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/presentation_delete.png and /dev/null differ diff --git a/resources/images/presentation_load.png b/resources/images/presentation_load.png deleted file mode 100644 index eb211e233..000000000 Binary files a/resources/images/presentation_load.png and /dev/null differ diff --git a/resources/images/projector_connectors.png b/resources/images/projector_connectors.png deleted file mode 100644 index 77a6a4dd7..000000000 Binary files a/resources/images/projector_connectors.png and /dev/null differ diff --git a/resources/images/projector_spacer.png b/resources/images/projector_spacer.png deleted file mode 100644 index ca5a54a04..000000000 Binary files a/resources/images/projector_spacer.png and /dev/null differ diff --git a/resources/images/projector_view.png b/resources/images/projector_view.png deleted file mode 100644 index e4d7e971b..000000000 Binary files a/resources/images/projector_view.png and /dev/null differ diff --git a/resources/images/service_delete.png b/resources/images/service_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/service_delete.png and /dev/null differ diff --git a/resources/images/service_edit.png b/resources/images/service_edit.png deleted file mode 100644 index 84e345d22..000000000 Binary files a/resources/images/service_edit.png and /dev/null differ diff --git a/resources/images/service_new.png b/resources/images/service_new.png deleted file mode 100644 index 8431237bd..000000000 Binary files a/resources/images/service_new.png and /dev/null differ diff --git a/resources/images/service_open.png b/resources/images/service_open.png deleted file mode 100644 index eb211e233..000000000 Binary files a/resources/images/service_open.png and /dev/null differ diff --git a/resources/images/service_save.png b/resources/images/service_save.png deleted file mode 100644 index 8072aea32..000000000 Binary files a/resources/images/service_save.png and /dev/null differ diff --git a/resources/images/slide_close.png b/resources/images/slide_close.png deleted file mode 100644 index 2c2f99eb4..000000000 Binary files a/resources/images/slide_close.png and /dev/null differ diff --git a/resources/images/song_delete.png b/resources/images/song_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/song_delete.png and /dev/null differ diff --git a/resources/images/song_edit.png b/resources/images/song_edit.png deleted file mode 100644 index 84e345d22..000000000 Binary files a/resources/images/song_edit.png and /dev/null differ diff --git a/resources/images/song_export.png b/resources/images/song_export.png deleted file mode 100644 index d26519b7f..000000000 Binary files a/resources/images/song_export.png and /dev/null differ diff --git a/resources/images/song_new.png b/resources/images/song_new.png deleted file mode 100644 index 8431237bd..000000000 Binary files a/resources/images/song_new.png and /dev/null differ diff --git a/resources/images/system_add.png b/resources/images/system_add.png deleted file mode 100644 index 7bb9b1be8..000000000 Binary files a/resources/images/system_add.png and /dev/null differ diff --git a/resources/images/system_live.png b/resources/images/system_live.png deleted file mode 100644 index cb2be2258..000000000 Binary files a/resources/images/system_live.png and /dev/null differ diff --git a/resources/images/system_preview.png b/resources/images/system_preview.png deleted file mode 100644 index d196792bb..000000000 Binary files a/resources/images/system_preview.png and /dev/null differ diff --git a/resources/images/theme_delete.png b/resources/images/theme_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/theme_delete.png and /dev/null differ diff --git a/resources/images/theme_export.png b/resources/images/theme_export.png deleted file mode 100644 index d26519b7f..000000000 Binary files a/resources/images/theme_export.png and /dev/null differ diff --git a/resources/images/theme_import.png b/resources/images/theme_import.png deleted file mode 100644 index 0ee789269..000000000 Binary files a/resources/images/theme_import.png and /dev/null differ diff --git a/resources/images/tools_alert.png b/resources/images/tools_alert.png deleted file mode 100644 index 331aa2687..000000000 Binary files a/resources/images/tools_alert.png and /dev/null differ diff --git a/resources/images/video_delete.png b/resources/images/video_delete.png deleted file mode 100644 index ef8e685e2..000000000 Binary files a/resources/images/video_delete.png and /dev/null differ diff --git a/resources/images/video_load.png b/resources/images/video_load.png deleted file mode 100644 index eb211e233..000000000 Binary files a/resources/images/video_load.png and /dev/null differ diff --git a/resources/images/wizard_createprojector.png b/resources/images/wizard_createprojector.png deleted file mode 100644 index d17a01e41..000000000 Binary files a/resources/images/wizard_createprojector.png and /dev/null differ diff --git a/resources/images/wizard_importsong.bmp b/resources/images/wizard_importsong.bmp deleted file mode 100644 index 948422dcc..000000000 Binary files a/resources/images/wizard_importsong.bmp and /dev/null differ diff --git a/resources/images/wizard_exportsong.bmp b/resources/images/wizard_song.bmp similarity index 100% rename from resources/images/wizard_exportsong.bmp rename to resources/images/wizard_song.bmp diff --git a/scripts/appveyor-webhook.py b/scripts/appveyor-webhook.py new file mode 100755 index 000000000..45438645d --- /dev/null +++ b/scripts/appveyor-webhook.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 script is used to trigger a build at appveyor. Since the code is not hosted +on github the normal triggering mechanisms can't be use. The project is +registered as subversion repository. A webhook is used to trigger new builds. +The appveyor.yml used for the build is send to appveyor when calling the hook. +""" +import json +import urllib +import urllib.request +import datetime +import sys +import time +from subprocess import Popen, PIPE + +appveyor_build_url = 'https://ci.appveyor.com/project/TomasGroth/openlp/build' +appveyor_api_url = 'https://ci.appveyor.com/api/projects/TomasGroth/openlp' + +webhook_element = \ + { + 'commit': { + 'author': { + 'email': 'contributer@openlp', + 'name': 'OpenLP Contributor' + }, + 'id': None, + 'message': None, + 'timestamp': datetime.datetime.now().isoformat() + }, + 'config': None, + 'repository': { + 'name': 'repo_name', + 'url': 'repo_url' + } + } + + +def get_version(): + """ + Get the version of the branch. + """ + bzr = Popen(('bzr', 'tags'), stdout=PIPE) + output = bzr.communicate()[0] + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr tags') + lines = output.splitlines() + if len(lines) == 0: + tag = '0.0.0' + revision = '0' + else: + tag, revision = lines[-1].decode('utf-8').split() + bzr = Popen(('bzr', 'log', '--line', '-r', '-1'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr log') + latest = output.decode('utf-8').split(':')[0] + version_string = latest == revision and tag or 'r%s' % latest + # Save decimal version in case we need to do a portable build. + version = latest == revision and tag or '%s.%s' % (tag, latest) + return version_string, version + + +def get_yml(branch): + """ + Returns the content of appveyor.yml and inserts the branch to be build + """ + f = open('appveyor.yml') + yml_text = f.read() + f.close() + yml_text = yml_text.replace('BRANCHNAME', branch) + if 'openlp-core/openlp/trunk' in branch: + yml_text = yml_text.replace('BUILD_DOCS', '$TRUE') + else: + yml_text = yml_text.replace('BUILD_DOCS', '$FALSE') + return yml_text + + +def hook(webhook_url, yml): + """ + Activate the webhook to start the build + """ + webhook_element['config'] = yml + webhook_element['commit']['message'] = 'Building ' + branch + version_string, version = get_version() + webhook_element['commit']['id'] = version_string + request = urllib.request.Request(webhook_url) + request.add_header('Content-Type', 'application/json;charset=utf-8') + responce = urllib.request.urlopen(request, json.dumps(webhook_element).encode('utf-8')) + if responce.getcode() != 204: + print('An error happened when calling the webhook! Return code: %d' % responce.getcode()) + print(responce.read().decode('utf-8')) + + +def get_appveyor_build_url(branch): + """ + Get the url of the build. + """ + responce = urllib.request.urlopen(appveyor_api_url) + json_str = responce.read().decode('utf-8') + build_json = json.loads(json_str) + build_url = '%s/%s' % (appveyor_build_url, build_json['build']['version']) + print('Check this URL for build status: %s' % build_url) + + +if len(sys.argv) != 3: + print('Usage: %s ' % sys.argv[0]) +else: + webhook_url = sys.argv[1] + branch = sys.argv[2] + hook(webhook_url, get_yml(branch)) + # Wait 5 seconds to make sure the hook has been triggered + time.sleep(5) + get_appveyor_build_url(branch) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml new file mode 100644 index 000000000..27b4cfc98 --- /dev/null +++ b/scripts/appveyor.yml @@ -0,0 +1,84 @@ +version: OpenLP-win-ci-b{build} + +init: + - choco install -y --force bzr + - set PATH=C:\Program Files (x86)\Bazaar;%PATH% + +clone_script: + - bzr checkout --lightweight BRANCHNAME openlp-branch + +environment: + PYTHON: C:\\Python34 + +install: + # Install dependencies from pypi + - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc psycopg2 pypiwin32 pyenchant" + # 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/) + - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/lxml-3.6.4-cp34-cp34m-win32.whl" + - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.5-cp34-cp34m-win32.whl" + # Download and install PyQt5 + - curl -L -O http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe + - PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S + # Download and unpack mupdf + - curl -O http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip + - 7z x mupdf-1.9a-windows.zip + - cp mupdf-1.9a-windows/mupdf.exe openlp-branch/mupdf.exe + # Download and unpack mediainfo + - curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip + - mkdir MediaInfo + - 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip + - cp MediaInfo\\MediaInfo.exe openlp-branch\\MediaInfo.exe + +build: off + +test_script: + - cd openlp-branch + # Run the tests + - "%PYTHON%\\python.exe -m nose -v tests" + # Go back to the user root folder + - cd.. + +after_test: + # This is where we create a package using PyInstaller + # First get PyInstaller + - curl -L -O https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip + - 7z x PyInstaller-3.2.zip + # Install PyInstaller dependencies + - "%PYTHON%\\python.exe -m pip install future pefile" + # Download and install Inno Setup - used for packaging + - curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe + - is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP- + # Download and unpack portable-bundle + - curl -O https://get.openlp.org/win-sdk/portable-setup.7z + - 7z x portable-setup.7z + # Disabled portable installers - can't figure out how to make them silent + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe + # - PortableApps.comInstaller_3.4.4.paf.exe /S + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Launcher/PortableApps.comLauncher_2.2.1.paf.exe + # - PortableApps.comLauncher_2.2.1.paf.exe /S + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe + # - NSISPortable_3.0_English.paf.exe /S + # Get the packaging code + - curl -L http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -o packaging.tar.gz + - 7z e packaging.tar.gz + - 7z x packaging.tar + - mv ~openlp-core/openlp/packaging packaging + # If this is trunk we should also build the manual + - ps: >- + If (BUILD_DOCS) { + &"$env:PYTHON\python.exe" -m pip install sphinx + Invoke-WebRequest -Uri "http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball" -OutFile documentation.tar.gz + 7z e documentation.tar.gz + 7z x documentation.tar + mv ~openlp-core/openlp/documentation documentation + cd packaging + &"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable + } else { + cd packaging + &"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch --portable + } + +artifacts: + - path: openlp-branch\dist\*.exe diff --git a/scripts/clean_up_resources.py b/scripts/clean_up_resources.py new file mode 100755 index 000000000..ce4cc8f6d --- /dev/null +++ b/scripts/clean_up_resources.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 fnmatch +import os + +from lxml import etree + +resource_path = os.path.join('..', 'resources', 'images') +resource_file_path = os.path.join(resource_path, 'openlp-2.qrc') +src_directory = os.path.join('..', 'openlp') + +RESOURCES_TO_IGNORE = [ + 'openlp.svg', + 'OpenLP.ico', + 'openlp-2.qrc', + 'openlp-logo-16x16.png', + 'openlp-logo-32x32.png', + 'openlp-logo-48x48.png', + 'openlp-logo-64x64.png', + 'openlp-logo-128x128.png', + 'README.txt' +] + +tree = etree.parse(resource_file_path) +root = tree.getroot() + +print('Looking for unused resources listed in openlp-2.qrc') +print('----------------------------------------------------------------') +resources = root.findall('.//file') +for current_dir, dirs, files in os.walk(src_directory): + for file_name in files: + if not fnmatch.fnmatch(file_name, '*.py'): + continue + file_path = os.path.join(current_dir, file_name) + with open(file_path) as source_file: + file_contents = source_file.read() + # Create a copy of the resources list so that we don't change the list while we're iterating through it! + for resource in list(resources): + if resource.text in file_contents: + resources.remove(resource) +if resources: + print('Unused resources listed in openlp-2.qrc:') + print(*(x.text for x in resources), sep='\n') + print('----------------------------------------------------------------') + remove = None + while remove != 'yes' and remove != 'no': + remove = input('Would you like to remove these files from openlp-2.qrc? (yes/no)') + if remove == 'yes': + for resource in resources: + parent = resource.find('..') + parent.remove(resource) + tree.write(resource_file_path, encoding='utf8') +else: + print('No unused resources listed in openlp-2.qrc') +print('----------------------------------------------------------------') + +print('\nLooking for resource files which are not lited in openlp-2.qrc') +print('----------------------------------------------------------------') +resources = [x.text for x in root.findall('.//file')] +removable_resources = [] +for resource_name in os.listdir(resource_path): + if resource_name not in RESOURCES_TO_IGNORE and resource_name not in resources: + removable_resources.append(resource_name) +if removable_resources: + print('Resource files not listed in openlp-2.qrc:') + print(*removable_resources, sep='\n') + print('----------------------------------------------------------------') + remove = None + while remove != 'yes' and remove != 'no': + remove = input('Would you like to delete these files from the resource folder? (yes/no)') + print('----------------------------------------------------------------') + if remove == 'yes': + for resource in removable_resources: + resource_file = os.path.join(resource_path, resource) + print('Removing {file}'.format(file=resource_file)) + os.remove(resource_file) +else: + print('All resource files are listed!') diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_common/test_httputils.py similarity index 71% rename from tests/functional/openlp_core_lib/test_webpagereader.py rename to tests/functional/openlp_core_common/test_httputils.py index 6e33fca51..98b24a994 100644 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -22,28 +22,40 @@ """ Functional tests to test the AppLocation class and related methods. """ +import os +import tempfile +import socket from unittest import TestCase -from openlp.core.lib.webpagereader import _get_user_agent, get_web_page +from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin -class TestUtils(TestCase): +class TestHttpUtils(TestCase, TestMixin): + """ - A test suite to test out various methods around the AppLocation class. + A test suite to test out various http helper functions. """ + def setUp(self): + self.tempfile = os.path.join(tempfile.gettempdir(), 'testfile') + + def tearDown(self): + if os.path.isfile(self.tempfile): + os.remove(self.tempfile) + def test_get_user_agent_linux(self): """ Test that getting a user agent on Linux returns a user agent suitable for Linux """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: + with patch('openlp.core.common.httputils.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'linux2' - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() + # WHEN: We call get_user_agent() + user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent result = 'Linux' in user_agent or 'CrOS' in user_agent @@ -53,13 +65,13 @@ class TestUtils(TestCase): """ Test that getting a user agent on Windows returns a user agent suitable for Windows """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: + with patch('openlp.core.common.httputils.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'win32' - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() + # WHEN: We call get_user_agent() + user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent') @@ -68,13 +80,13 @@ class TestUtils(TestCase): """ Test that getting a user agent on OS X returns a user agent suitable for OS X """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: + with patch('openlp.core.common.httputils.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'darwin' - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() + # WHEN: We call get_user_agent() + user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent') @@ -83,13 +95,13 @@ class TestUtils(TestCase): """ Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: + with patch('openlp.core.common.httputils.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'freebsd' - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() + # WHEN: We call get_user_agent() + user_agent = get_user_agent() # THEN: The user agent is a Linux (or ChromeOS) user agent self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent') @@ -111,9 +123,9 @@ class TestUtils(TestCase): """ Test that the get_web_page method works correctly """ - with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ + 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() @@ -141,9 +153,9 @@ class TestUtils(TestCase): """ Test that adding a header to the call to get_web_page() adds the header to the request """ - with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: + 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 @@ -170,9 +182,9 @@ class TestUtils(TestCase): """ 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.lib.webpagereader.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: + 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 @@ -189,7 +201,7 @@ class TestUtils(TestCase): 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') + 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') @@ -198,10 +210,10 @@ class TestUtils(TestCase): """ Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() """ - with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.lib.webpagereader.Registry') as MockRegistry: + 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 @@ -227,3 +239,36 @@ class TestUtils(TestCase): 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') + + def test_get_url_file_size(self): + """ + 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.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' + + # WHEN: The get_url_file_size() method is called + 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) + + @patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') + def test_socket_timeout(self, mocked_urlopen): + """ + 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() + + # WHEN: Attempt to retrieve a file + url_get_file(MagicMock(), url='http://localhost/test', f_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') diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 145be21f4..fdab52e93 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -189,16 +189,14 @@ class TestLib(TestCase): """ Test the build_icon() function with a QIcon instance """ - with patch('openlp.core.lib.QtGui') as MockedQtGui: - # GIVEN: A mocked QIcon - MockedQtGui.QIcon = MagicMock - mocked_icon = MockedQtGui.QIcon() + # GIVEN: An icon QIcon + icon = QtGui.QIcon() - # WHEN: We pass a QIcon instance in - result = build_icon(mocked_icon) + # WHEN: We pass a QIcon instance in + result = build_icon(icon) - # THEN: The result should be our mocked QIcon - self.assertIs(mocked_icon, result, 'The result should be the mocked QIcon') + # THEN: The result should be the same icon as we passed in + self.assertIs(icon, result, 'The result should be the same icon as we passed in') def test_build_icon_with_resource(self): """ @@ -223,6 +221,30 @@ class TestLib(TestCase): self.assertIsInstance(result, MagicMock, 'The result should be a MagicMock, because we mocked it out') def test_image_to_byte(self): + """ + Test the image_to_byte() function + """ + with patch('openlp.core.lib.QtCore') as MockedQtCore: + # GIVEN: A set of mocked-out Qt classes + mocked_byte_array = MagicMock() + MockedQtCore.QByteArray.return_value = mocked_byte_array + mocked_buffer = MagicMock() + MockedQtCore.QBuffer.return_value = mocked_buffer + MockedQtCore.QIODevice.WriteOnly = 'writeonly' + mocked_image = MagicMock() + + # WHEN: We convert an image to a byte array + result = image_to_byte(mocked_image, base_64=False) + + # THEN: We should receive the mocked_buffer + MockedQtCore.QByteArray.assert_called_with() + MockedQtCore.QBuffer.assert_called_with(mocked_byte_array) + mocked_buffer.open.assert_called_with('writeonly') + mocked_image.save.assert_called_with(mocked_buffer, "PNG") + self.assertFalse(mocked_byte_array.toBase64.called) + self.assertEqual(mocked_byte_array, result, 'The mocked out byte array should be returned') + + def test_image_to_byte_base_64(self): """ Test the image_to_byte() function """ @@ -666,8 +688,8 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We should have "Author 1, Author 2, and Author 3" - assert string_result == 'Author 1, Author 2, and Author 3', 'The string should be u\'Author 1, ' \ - 'Author 2, and Author 3\'.' + self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, ' + 'Author 2, and Author 3".') def test_create_separated_list_empty_list(self): """ @@ -683,56 +705,44 @@ class TestLib(TestCase): string_result = create_separated_list(string_list) # THEN: We shoud have an emptry string. - assert string_result == '', 'The string sould be empty.' + self.assertEqual(string_result, '', 'The string sould be empty.') def test_create_separated_list_with_one_item(self): """ Test the create_separated_list function with a list consisting of only one entry """ - with patch('openlp.core.lib.Qt') as mocked_qt: - # GIVEN: A list with a string and the mocked Qt module. - mocked_qt.PYQT_VERSION_STR = '4.8' - mocked_qt.qVersion.return_value = '4.7' - string_list = ['Author 1'] + # GIVEN: A list with a string. + string_list = ['Author 1'] - # WHEN: We get a string build from the entries it the list and a separator. - string_result = create_separated_list(string_list) + # WHEN: We get a string build from the entries it the list and a separator. + string_result = create_separated_list(string_list) - # THEN: We should have "Author 1" - assert string_result == 'Author 1', 'The string should be u\'Author 1\'.' + # THEN: We should have "Author 1" + self.assertEqual(string_result, 'Author 1', 'The string should be "Author 1".') def test_create_separated_list_with_two_items(self): """ Test the create_separated_list function with a list of two entries """ - with patch('openlp.core.lib.Qt') as mocked_qt, patch('openlp.core.lib.translate') as mocked_translate: - # GIVEN: A list of strings and the mocked Qt module. - mocked_qt.PYQT_VERSION_STR = '4.8' - mocked_qt.qVersion.return_value = '4.7' - mocked_translate.return_value = '%s and %s' - string_list = ['Author 1', 'Author 2'] + # GIVEN: A list with two strings. + string_list = ['Author 1', 'Author 2'] - # WHEN: We get a string build from the entries it the list and a seperator. - string_result = create_separated_list(string_list) + # WHEN: We get a string build from the entries it the list and a seperator. + string_result = create_separated_list(string_list) - # THEN: We should have "Author 1 and Author 2" - assert string_result == 'Author 1 and Author 2', 'The string should be u\'Author 1 and Author 2\'.' + # THEN: We should have "Author 1 and Author 2" + self.assertEqual(string_result, 'Author 1 and Author 2', 'The string should be "Author 1 and Author 2".') def test_create_separated_list_with_three_items(self): """ Test the create_separated_list function with a list of three items """ - with patch('openlp.core.lib.Qt') as mocked_qt, patch('openlp.core.lib.translate') as mocked_translate: - # GIVEN: A list with a string and the mocked Qt module. - mocked_qt.PYQT_VERSION_STR = '4.8' - mocked_qt.qVersion.return_value = '4.7' - # Always return the untranslated string. - mocked_translate.side_effect = lambda module, string_to_translate, comment: string_to_translate - string_list = ['Author 1', 'Author 2', 'Author 3'] + # GIVEN: A list with three strings. + string_list = ['Author 1', 'Author 2', 'Author 3'] - # WHEN: We get a string build from the entries it the list and a seperator. - string_result = create_separated_list(string_list) + # WHEN: We get a string build from the entries it the list and a seperator. + string_result = create_separated_list(string_list) - # THEN: We should have "Author 1, Author 2, and Author 3" - assert string_result == 'Author 1, Author 2, and Author 3', 'The string should be u\'Author 1, ' \ - 'Author 2, and Author 3\'.' + # THEN: We should have "Author 1, Author 2 and Author 3" + self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, ' + 'Author 2, and Author 3".') diff --git a/tests/functional/openlp_core_ui/test_aboutform.py b/tests/functional/openlp_core_ui/test_aboutform.py index 612c6b887..47a685f9d 100644 --- a/tests/functional/openlp_core_ui/test_aboutform.py +++ b/tests/functional/openlp_core_ui/test_aboutform.py @@ -32,16 +32,30 @@ from tests.helpers.testmixin import TestMixin class TestFirstTimeForm(TestCase, TestMixin): - def test_on_volunteer_button_clicked(self): + @patch('openlp.core.ui.aboutform.webbrowser') + def test_on_volunteer_button_clicked(self, mocked_webbrowser): """ Test that clicking on the "Volunteer" button opens a web page. """ # GIVEN: A new About dialog and a mocked out webbrowser module - with patch('openlp.core.ui.aboutform.webbrowser') as mocked_webbrowser: - about_form = AboutForm(None) + about_form = AboutForm(None) - # WHEN: The "Volunteer" button is "clicked" - about_form.on_volunteer_button_clicked() + # WHEN: The "Volunteer" button is "clicked" + about_form.on_volunteer_button_clicked() - # THEN: A web browser is opened - mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute') + # 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): + """ + 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'} + + # WHEN: The about form is created + about_form = AboutForm(None) + + # THEN: The build number should be in the text + assert 'OpenLP 3.1.5 build 3000' in about_form.about_text_edit.toPlainText() diff --git a/tests/functional/openlp_core_ui/test_first_time.py b/tests/functional/openlp_core_ui/test_first_time.py index d8067dfbe..f23bf4db6 100644 --- a/tests/functional/openlp_core_ui/test_first_time.py +++ b/tests/functional/openlp_core_ui/test_first_time.py @@ -31,7 +31,7 @@ import urllib.parse from tests.functional import patch from tests.helpers.testmixin import TestMixin -from openlp.core.lib.webpagereader import CONNECTION_RETRIES, get_web_page +from openlp.core.common.httputils import CONNECTION_RETRIES, get_web_page class TestFirstTimeWizard(TestMixin, TestCase): diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 5dd1430cd..ec26f60fe 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -23,7 +23,6 @@ Package to test the openlp.core.ui.firsttimeform package. """ import os -import socket import tempfile import urllib from unittest import TestCase @@ -236,20 +235,3 @@ class TestFirstTimeForm(TestCase, TestMixin): # 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') - - @patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') - def test_socket_timeout(self, mocked_urlopen): - """ - Test socket timeout gets caught - """ - # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download - first_time_form = FirstTimeForm(None) - first_time_form.initialize(MagicMock()) - mocked_urlopen.side_effect = socket.timeout() - - # WHEN: Attempt to retrieve a file - first_time_form.url_get_file(url='http://localhost/test', f_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') diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 5aa0f42a4..8b10f6e0f 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -270,6 +270,8 @@ class TestMainDisplay(TestCase, TestMixin): service_item = MagicMock() 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' mocked_plugin = MagicMock() display.plugin_manager = PluginManager() display.plugin_manager.plugins = [mocked_plugin] diff --git a/tests/functional/openlp_core_ui/test_settingsform.py b/tests/functional/openlp_core_ui/test_settingsform.py index 051bc7af1..3f6f0aaad 100644 --- a/tests/functional/openlp_core_ui/test_settingsform.py +++ b/tests/functional/openlp_core_ui/test_settingsform.py @@ -48,7 +48,7 @@ class TestSettingsForm(TestCase): general_tab = MagicMock() general_tab.tab_title = 'mock' general_tab.tab_title_visible = 'Mock' - general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + general_tab.icon_path = ':/icon/openlp-logo.svg' # WHEN: We insert the general tab with patch.object(settings_form.stacked_layout, 'addWidget') as mocked_add_widget, \ @@ -86,14 +86,14 @@ class TestSettingsForm(TestCase): general_tab = QtWidgets.QWidget(None) general_tab.tab_title = 'mock-general' general_tab.tab_title_visible = 'Mock General' - general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + general_tab.icon_path = ':/icon/openlp-logo.svg' mocked_general_save = MagicMock() general_tab.save = mocked_general_save settings_form.insert_tab(general_tab, is_visible=True) themes_tab = QtWidgets.QWidget(None) themes_tab.tab_title = 'mock-themes' themes_tab.tab_title_visible = 'Mock Themes' - themes_tab.icon_path = ':/icon/openlp-logo-16x16.png' + themes_tab.icon_path = ':/icon/openlp-logo.svg' mocked_theme_save = MagicMock() themes_tab.save = mocked_theme_save settings_form.insert_tab(themes_tab, is_visible=False) @@ -114,7 +114,7 @@ class TestSettingsForm(TestCase): general_tab = QtWidgets.QWidget(None) general_tab.tab_title = 'mock' general_tab.tab_title_visible = 'Mock' - general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + general_tab.icon_path = ':/icon/openlp-logo.svg' settings_form.insert_tab(general_tab, is_visible=True) with patch.object(settings_form.stacked_layout, 'count') as mocked_count: @@ -133,14 +133,14 @@ class TestSettingsForm(TestCase): general_tab = QtWidgets.QWidget(None) general_tab.tab_title = 'mock-general' general_tab.tab_title_visible = 'Mock General' - general_tab.icon_path = ':/icon/openlp-logo-16x16.png' + general_tab.icon_path = ':/icon/openlp-logo.svg' mocked_general_cancel = MagicMock() general_tab.cancel = mocked_general_cancel settings_form.insert_tab(general_tab, is_visible=True) themes_tab = QtWidgets.QWidget(None) themes_tab.tab_title = 'mock-themes' themes_tab.tab_title_visible = 'Mock Themes' - themes_tab.icon_path = ':/icon/openlp-logo-16x16.png' + themes_tab.icon_path = ':/icon/openlp-logo.svg' mocked_theme_cancel = MagicMock() themes_tab.cancel = mocked_theme_cancel settings_form.insert_tab(themes_tab, is_visible=False) diff --git a/tests/functional/openlp_core_ui/test_shortcutlistdialog.py b/tests/functional/openlp_core_ui/test_shortcutlistdialog.py new file mode 100644 index 000000000..2f39cb34a --- /dev/null +++ b/tests/functional/openlp_core_ui/test_shortcutlistdialog.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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.ui.shortcutlistdialog package. +""" +from PyQt5 import QtCore, QtGui, QtWidgets + +from openlp.core.ui.shortcutlistdialog import CaptureShortcutButton, ShortcutTreeWidget + +from tests.interfaces import MagicMock, patch + + +def test_key_press_event(): + """ + Test the keyPressEvent method + """ + # GIVEN: A checked button and a mocked event + button = CaptureShortcutButton() + button.setChecked(True) + mocked_event = MagicMock() + mocked_event.key.return_value = QtCore.Qt.Key_Space + + # WHEN: keyPressEvent is called with an event that should be ignored + button.keyPressEvent(mocked_event) + + # THEN: The ignore() method on the event should have been called + mocked_event.ignore.assert_called_once_with() + + +def test_keyboard_search(): + """ + Test the keyboardSearch method of the ShortcutTreeWidget + """ + # GIVEN: A ShortcutTreeWidget + widget = ShortcutTreeWidget() + + # WHEN: keyboardSearch() is called + widget.keyboardSearch('') + + # THEN: Nothing happens + assert True diff --git a/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py b/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py new file mode 100644 index 000000000..f6cc8b446 --- /dev/null +++ b/tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 openlp.core.lib.listwidgetwithdnd module +""" +from unittest import TestCase + +from openlp.core.common.uistrings import UiStrings +from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD +from unittest.mock import MagicMock, patch + + +class TestListWidgetWithDnD(TestCase): + """ + Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class + """ + def test_clear(self): + """ + Test the clear method when called without any arguments. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with out any arguments + widget.clear() + + # THEN: The results text should be the standard 'no results' text. + self.assertEqual(widget.no_results_text, UiStrings().NoResults) + + def test_clear_search_while_typing(self): + """ + Test the clear method when called with the search_while_typing argument set to True + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with search_while_typing set to True + widget.clear(search_while_typing=True) + + # THEN: The results text should be the 'short results' text. + self.assertEqual(widget.no_results_text, UiStrings().ShortResults) + + def test_paint_event(self): + """ + Test the paintEvent method when the list is not empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 1 + # (i.e the list has an item) + widget = ListWidgetWithDnD() + with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=1), \ + patch.object(widget, 'viewport') as mocked_viewport: + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called + mocked_paint_event.assert_called_once_with(mocked_event) + self.assertFalse(mocked_viewport.called) + + def test_paint_event_no_items(self): + """ + Test the paintEvent method when the list is empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 0 + # (i.e the list is empty) + widget = ListWidgetWithDnD() + mocked_painter_instance = MagicMock() + mocked_qrect = MagicMock() + with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'viewport'), \ + patch('openlp.core.ui.lib.listwidgetwithdnd.QtGui.QPainter', + return_value=mocked_painter_instance) as mocked_qpainter, \ + patch('openlp.core.ui.lib.listwidgetwithdnd.QtCore.QRect', return_value=mocked_qrect): + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called, and some text should be drawn. + mocked_paint_event.assert_called_once_with(mocked_event) + mocked_qpainter.assert_called_once_with(widget.viewport()) + mocked_painter_instance.drawText.assert_called_once_with(mocked_qrect, 4100, 'No Search Results') diff --git a/tests/functional/openlp_plugins/bibles/test_manager.py b/tests/functional/openlp_plugins/bibles/test_manager.py new file mode 100644 index 000000000..d18d3cfc6 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_manager.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-2016 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 manager submodule of the Bibles plugin. +""" +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from openlp.plugins.bibles.lib.manager import BibleManager + + +class TestManager(TestCase): + """ + Test the functions in the :mod:`manager` module. + """ + + def setUp(self): + app_location_patcher = patch('openlp.plugins.bibles.lib.manager.AppLocation') + self.addCleanup(app_location_patcher.stop) + app_location_patcher.start() + log_patcher = patch('openlp.plugins.bibles.lib.manager.log') + self.addCleanup(log_patcher.stop) + self.mocked_log = log_patcher.start() + settings_patcher = patch('openlp.plugins.bibles.lib.manager.Settings') + self.addCleanup(settings_patcher.stop) + settings_patcher.start() + + def test_delete_bible(self): + """ + Test the BibleManager delete_bible method + """ + # GIVEN: An instance of BibleManager and a mocked bible + with patch.object(BibleManager, 'reload_bibles'), \ + patch('openlp.plugins.bibles.lib.manager.os.path.join', side_effect=lambda x, y: '{}/{}'.format(x, y)),\ + patch('openlp.plugins.bibles.lib.manager.delete_file', return_value=True) as mocked_delete_file: + instance = BibleManager(MagicMock()) + # We need to keep a reference to the mock for close_all as it gets set to None later on! + mocked_close_all = MagicMock() + mocked_bible = MagicMock(file='KJV.sqlite', path='bibles', **{'session.close_all': mocked_close_all}) + instance.db_cache = {'KJV': mocked_bible} + + # WHEN: Calling delete_bible with 'KJV' + result = instance.delete_bible('KJV') + + # THEN: The session should have been closed and set to None, the bible should be deleted, and the result of + # the deletion returned. + self.assertTrue(result) + mocked_close_all.assert_called_once_with() + self.assertIsNone(mocked_bible.session) + mocked_delete_file.assert_called_once_with('bibles/KJV.sqlite') diff --git a/tests/functional/openlp_plugins/bibles/test_mediaitem.py b/tests/functional/openlp_plugins/bibles/test_mediaitem.py index 28a755fd6..d4a6eee39 100644 --- a/tests/functional/openlp_plugins/bibles/test_mediaitem.py +++ b/tests/functional/openlp_plugins/bibles/test_mediaitem.py @@ -155,7 +155,6 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.list_view = MagicMock() self.media_item.search_results = MagicMock() self.media_item.display_results = MagicMock() - self.media_item.check_search_result = MagicMock() self.app.set_normal_cursor = MagicMock() # WHEN: on_quick_search_button is called @@ -169,7 +168,6 @@ class TestMediaItem(TestCase, TestMixin): self.assertEqual(1, self.media_item.quickLockButton.isChecked.call_count, 'Lock Should had been called once') self.assertEqual(1, self.media_item.display_results.call_count, 'Display results Should had been called once') self.assertEqual(2, self.media_item.quickSearchButton.setEnabled.call_count, 'Disable and Enable the button') - self.assertEqual(1, self.media_item.check_search_result.call_count, 'Check results Should had been called once') self.assertEqual(1, self.app.set_normal_cursor.call_count, 'Normal cursor should had been called once') def test_on_clear_button_clicked(self): @@ -178,7 +176,6 @@ class TestMediaItem(TestCase, TestMixin): """ # GIVEN: Mocked list_view, check_search_results & quick_search_edit. self.media_item.list_view = MagicMock() - self.media_item.check_search_result = MagicMock() self.media_item.quick_search_edit = MagicMock() # WHEN: on_clear_button_clicked is called @@ -186,7 +183,6 @@ class TestMediaItem(TestCase, TestMixin): # THEN: Search result should be reset and search field should receive focus. self.media_item.list_view.clear.assert_called_once_with(), - self.media_item.check_search_result.assert_called_once_with(), self.media_item.quick_search_edit.clear.assert_called_once_with(), self.media_item.quick_search_edit.setFocus.assert_called_once_with() diff --git a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py new file mode 100644 index 000000000..622f83fa8 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 WordProject Bible importer. +""" + +import os +import json +from unittest import TestCase + +from openlp.plugins.bibles.lib.importers.wordproject import WordProjectBible +from openlp.plugins.bibles.lib.db import BibleDB + +from tests.functional import MagicMock, patch, call + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) +INDEX_PAGE = open(os.path.join(TEST_PATH, 'wordproject_index.htm')).read() +CHAPTER_PAGE = open(os.path.join(TEST_PATH, 'wordproject_chapter.htm')).read() + + +class TestWordProjectImport(TestCase): + """ + Test the functions in the :mod:`wordprojectimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry') + self.addCleanup(self.registry_patcher.stop) + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.addCleanup(self.manager_patcher.stop) + self.manager_patcher.start() + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_books(self, mocked_open, mocked_os): + """ + Test the process_books() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = INDEX_PAGE + mocked_os.path.join.side_effect = lambda *x: ''.join(x) + + # WHEN: process_books() is called + with patch.object(importer, 'find_and_create_book') as mocked_find_and_create_book, \ + patch.object(importer, 'process_chapters') as mocked_process_chapters, \ + patch.object(importer, 'session') as mocked_session: + importer.process_books() + + # THEN: The right methods should have been called + mocked_os.path.join.assert_called_once_with('', 'index.htm') + mocked_open.assert_called_once_with('index.htm', encoding='utf-8', errors='ignore') + assert mocked_find_and_create_book.call_count == 66, 'There should be 66 books' + assert mocked_process_chapters.call_count == 66, 'There should be 66 books' + assert mocked_session.commit.call_count == 66, 'There should be 66 books' + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_chapters(self, mocked_open, mocked_os): + """ + Test the process_chapters() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE + mocked_os.path.join.side_effect = lambda *x: ''.join(x) + mocked_os.path.normpath.side_effect = lambda x: x + mocked_db_book = MagicMock() + mocked_db_book.name = 'Genesis' + book_id = 1 + book_link = '01/1.htm' + + # WHEN: process_chapters() is called + with patch.object(importer, 'set_current_chapter') as mocked_set_current_chapter, \ + patch.object(importer, 'process_verses') as mocked_process_verses: + importer.process_chapters(mocked_db_book, book_id, book_link) + + # THEN: The right methods should have been called + expected_set_current_chapter_calls = [call('Genesis', ch) for ch in range(1, 51)] + expected_process_verses_calls = [call(mocked_db_book, 1, ch) for ch in range(1, 51)] + mocked_os.path.join.assert_called_once_with('', '01/1.htm') + mocked_open.assert_called_once_with('01/1.htm', encoding='utf-8', errors='ignore') + assert mocked_set_current_chapter.call_args_list == expected_set_current_chapter_calls + assert mocked_process_verses.call_args_list == expected_process_verses_calls + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_verses(self, mocked_open, mocked_os): + """ + Test the process_verses() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE + mocked_os.path.join.side_effect = lambda *x: '/'.join(x) + mocked_db_book = MagicMock() + mocked_db_book.name = 'Genesis' + book_number = 1 + chapter_number = 1 + + # WHEN: process_verses() is called + with patch.object(importer, 'process_verse') as mocked_process_verse: + importer.process_verses(mocked_db_book, book_number, chapter_number) + + # THEN: All the right methods should have been called + mocked_os.path.join.assert_called_once_with('', '01', '1.htm') + mocked_open.assert_called_once_with('/01/1.htm', encoding='utf-8', errors='ignore') + assert mocked_process_verse.call_count == 31 + + def test_process_verse(self): + """ + Test the process_verse() method + """ + # GIVEN: An importer and a mocked method + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + mocked_db_book = MagicMock() + mocked_db_book.id = 1 + chapter_number = 1 + verse_number = 1 + verse_text = ' In the beginning, God created the heavens and the earth ' + + # WHEN: process_verse() is called + with patch.object(importer, 'create_verse') as mocked_create_verse: + importer.process_verse(mocked_db_book, chapter_number, verse_number, verse_text) + + # THEN: The create_verse() method should have been called + mocked_create_verse.assert_called_once_with(1, 1, 1, 'In the beginning, God created the heavens and the earth') + + def test_process_verse_no_text(self): + """ + Test the process_verse() method when there's no text + """ + # GIVEN: An importer and a mocked method + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + mocked_db_book = MagicMock() + mocked_db_book.id = 1 + chapter_number = 1 + verse_number = 1 + verse_text = '' + + # WHEN: process_verse() is called + with patch.object(importer, 'create_verse') as mocked_create_verse: + importer.process_verse(mocked_db_book, chapter_number, verse_number, verse_text) + + # THEN: The create_verse() method should NOT have been called + assert mocked_create_verse.call_count == 0 + + def test_do_import(self): + """ + Test the do_import() method + """ + # GIVEN: An importer and mocked methods + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + + # WHEN: do_import() is called + with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ + patch.object(importer, 'get_language_id') as mocked_get_language_id, \ + patch.object(importer, 'process_books') as mocked_process_books, \ + patch.object(importer, '_cleanup') as mocked_cleanup: + mocked_get_language_id.return_value = 1 + result = importer.do_import() + + # THEN: The correct methods should have been called + mocked_unzip_file.assert_called_once_with() + mocked_get_language_id.assert_called_once_with(None, bible_name='kj.zip') + mocked_process_books.assert_called_once_with() + mocked_cleanup.assert_called_once_with() + assert result is True + + def test_do_import_no_language(self): + """ + Test the do_import() method when the language is not available + """ + # GIVEN: An importer and mocked methods + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + + # WHEN: do_import() is called + with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ + patch.object(importer, 'get_language_id') as mocked_get_language_id, \ + patch.object(importer, 'process_books') as mocked_process_books, \ + patch.object(importer, '_cleanup') as mocked_cleanup: + mocked_get_language_id.return_value = None + result = importer.do_import() + + # THEN: The correct methods should have been called + mocked_unzip_file.assert_called_once_with() + mocked_get_language_id.assert_called_once_with(None, bible_name='kj.zip') + assert mocked_process_books.call_count == 0 + mocked_cleanup.assert_called_once_with() + assert result is False diff --git a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py index 3666eac40..824951a66 100644 --- a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py @@ -137,7 +137,7 @@ class TestPowerpointDocument(TestCase, TestMixin): instance.goto_slide(42) # THEN: mocked_critical_error_message_box should have been called - mocked_critical_error_message_box.assert_called_with('Error', 'An error occurred in the Powerpoint ' + mocked_critical_error_message_box.assert_called_with('Error', 'An error occurred in the PowerPoint ' 'integration and the presentation will be stopped.' ' Restart the presentation if you wish to ' 'present it.') diff --git a/tests/functional/openlp_plugins/songs/test_editsongform.py b/tests/functional/openlp_plugins/songs/test_editsongform.py index 184c59717..ba53fa525 100644 --- a/tests/functional/openlp_plugins/songs/test_editsongform.py +++ b/tests/functional/openlp_plugins/songs/test_editsongform.py @@ -76,3 +76,34 @@ class TestEditSongForm(TestCase, TestMixin): # THEN they should be valid self.assertTrue(valid, "The tags list should be valid") + + @patch('openlp.plugins.songs.forms.editsongform.set_case_insensitive_completer') + def test_load_objects(self, mocked_set_case_insensitive_completer): + """ + Test the _load_objects() method + """ + # GIVEN: A song edit form and some mocked stuff + mocked_class = MagicMock() + mocked_class.name = 'Author' + mocked_combo = MagicMock() + mocked_combo.count.return_value = 0 + mocked_cache = MagicMock() + mocked_object = MagicMock() + mocked_object.name = 'Charles' + mocked_object.id = 1 + mocked_manager = MagicMock() + mocked_manager.get_all_objects.return_value = [mocked_object] + self.edit_song_form.manager = mocked_manager + + # WHEN: _load_objects() is called + self.edit_song_form._load_objects(mocked_class, mocked_combo, mocked_cache) + + # THEN: All the correct methods should have been called + self.edit_song_form.manager.get_all_objects.assert_called_once_with(mocked_class) + mocked_combo.clear.assert_called_once_with() + mocked_combo.count.assert_called_once_with() + mocked_combo.addItem.assert_called_once_with('Charles') + mocked_cache.append.assert_called_once_with('Charles') + mocked_combo.setItemData.assert_called_once_with(0, 1) + mocked_set_case_insensitive_completer.assert_called_once_with(mocked_cache, mocked_combo) + mocked_combo.setEditText.assert_called_once_with('') diff --git a/tests/functional/test_init.py b/tests/functional/test_init.py index 825de57f3..504489416 100644 --- a/tests/functional/test_init.py +++ b/tests/functional/test_init.py @@ -102,7 +102,7 @@ class TestInit(TestCase, TestMixin): mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created - self.openlp.backup_on_upgrade(old_install) + self.openlp.backup_on_upgrade(old_install, False) # THEN: It should not ask if we want to create a backup self.assertEqual(Settings().value('core/application version'), '2.2.0', 'Version should be the same!') @@ -120,14 +120,18 @@ class TestInit(TestCase, TestMixin): 'build': 'bzr000' } 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,\ patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: mocked_get_application_version.return_value = MOCKED_VERSION mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created - self.openlp.backup_on_upgrade(old_install) + self.openlp.backup_on_upgrade(old_install, True) # THEN: It should ask if we want to create a backup self.assertEqual(Settings().value('core/application version'), '2.2.0', 'Version should be upgraded!') self.assertEqual(mocked_question.call_count, 1, 'A question should have been asked!') + self.openlp.splash.hide.assert_called_once_with() + self.openlp.splash.show.assert_called_once_with() diff --git a/tests/interfaces/openlp_core_lib/test_searchedit.py b/tests/interfaces/openlp_core_lib/test_searchedit.py index a4c4472f5..464c6217d 100644 --- a/tests/interfaces/openlp_core_lib/test_searchedit.py +++ b/tests/interfaces/openlp_core_lib/test_searchedit.py @@ -23,6 +23,7 @@ Module to test the EditCustomForm. """ from unittest import TestCase +from unittest.mock import MagicMock, call, patch from PyQt5 import QtCore, QtGui, QtTest, QtWidgets @@ -58,7 +59,12 @@ class TestSearchEdit(TestCase, TestMixin): self.main_window = QtWidgets.QMainWindow() Registry().register('main_window', self.main_window) - self.search_edit = SearchEdit(self.main_window) + settings_patcher = patch( + 'openlp.core.lib.searchedit.Settings', return_value=MagicMock(**{'value.return_value': SearchTypes.First})) + self.addCleanup(settings_patcher.stop) + self.mocked_settings = settings_patcher.start() + + self.search_edit = SearchEdit(self.main_window, 'settings_section') # To complete set up we have to set the search types. self.search_edit.set_search_types(SEARCH_TYPES) @@ -78,8 +84,11 @@ class TestSearchEdit(TestCase, TestMixin): # WHEN: - # THEN: The first search type should be the first one in the list. - assert self.search_edit.current_search_type() == SearchTypes.First, "The first search type should be selected." + # THEN: The first search type should be the first one in the list. The selected type should be saved in the + # settings + self.assertEqual(self.search_edit.current_search_type(), SearchTypes.First, + "The first search type should be selected.") + self.mocked_settings().setValue.assert_called_once_with('settings_section/last search type', 0) def test_set_current_search_type(self): """ @@ -90,11 +99,13 @@ class TestSearchEdit(TestCase, TestMixin): result = self.search_edit.set_current_search_type(SearchTypes.Second) # THEN: - assert result, "The call should return success (True)." - assert self.search_edit.current_search_type() == SearchTypes.Second,\ - "The search type should be SearchTypes.Second" - assert self.search_edit.placeholderText() == SECOND_PLACEHOLDER_TEXT,\ - "The correct placeholder text should be 'Second Placeholder Text'." + self.assertTrue(result, "The call should return success (True).") + self.assertEqual(self.search_edit.current_search_type(), SearchTypes.Second, + "The search type should be SearchTypes.Second") + self.assertEqual(self.search_edit.placeholderText(), SECOND_PLACEHOLDER_TEXT, + "The correct placeholder text should be 'Second Placeholder Text'.") + self.mocked_settings().setValue.assert_has_calls( + [call('settings_section/last search type', 0), call('settings_section/last search type', 1)]) def test_clear_button_visibility(self): """ diff --git a/tests/resources/bibles/wordproject_chapter.htm b/tests/resources/bibles/wordproject_chapter.htm new file mode 100644 index 000000000..fb9b8a272 --- /dev/null +++ b/tests/resources/bibles/wordproject_chapter.htm @@ -0,0 +1,248 @@ + + + + + Creation of the world, Genesis Chapter 1 + + + + + + + + + + + + + + + + + + + + + +
+
+
+

WordProject

+
+
+
+ + +
+
+ +
+ +facebook + +twitter + +google + +linkin

+
+ +
+ + +
+
+
+
+
+
+
+
+

Genesis

+ +

Chapter: + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 + +

+
+ + + +
+
+

Chapter 1

+ + +

1 In the beginning God created the heaven and the earth. +
2 And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. +
3 And God said, Let there be light: and there was light. +
4 And God saw the light, that it was good: and God divided the light from the darkness. +
5 And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day. +
6 And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters. +
7 And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. +
8 And God called the firmament Heaven. And the evening and the morning were the second day. +
9 And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. +
10 And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. +
11 And God said, Let the earth bring forth grass, the herb yielding seed, and the fruit tree yielding fruit after his kind, whose seed is in itself, upon the earth: and it was so. +
12 And the earth brought forth grass, and herb yielding seed after his kind, and the tree yielding fruit, whose seed was in itself, after his kind: and God saw that it was good. +
13 And the evening and the morning were the third day. +
14 And God said, Let there be lights in the firmament of the heaven to divide the day from the night; and let them be for signs, and for seasons, and for days, and years: +
15 And let them be for lights in the firmament of the heaven to give light upon the earth: and it was so. +
16 And God made two great lights; the greater light to rule the day, and the lesser light to rule the night: he made the stars also. +
17 And God set them in the firmament of the heaven to give light upon the earth, +
18 And to rule over the day and over the night, and to divide the light from the darkness: and God saw that it was good. +
19 And the evening and the morning were the fourth day. +
20 And God said, Let the waters bring forth abundantly the moving creature that hath life, and fowl that may fly above the earth in the open firmament of heaven. +
21 And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good. +
22 And God blessed them, saying, Be fruitful, and multiply, and fill the waters in the seas, and let fowl multiply in the earth. +
23 And the evening and the morning were the fifth day. +
24 And God said, Let the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the earth after his kind: and it was so. +
25 And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good. +
26 And God said, Let us make man in our image, after our likeness: and let them have dominion over the fish of the sea, and over the fowl of the air, and over the cattle, and over all the earth, and over every creeping thing that creepeth upon the earth. +
27 So God created man in his own image, in the image of God created he him; male and female created he them. +
28 And God blessed them, and God said unto them, Be fruitful, and multiply, and replenish the earth, and subdue it: and have dominion over the fish of the sea, and over the fowl of the air, and over every living thing that moveth upon the earth. +
29 And God said, Behold, I have given you every herb bearing seed, which is upon the face of all the earth, and every tree, in the which is the fruit of a tree yielding seed; to you it shall be for meat. +
30 And to every beast of the earth, and to every fowl of the air, and to every thing that creepeth upon the earth, wherein there is life, I have given every green herb for meat: and it was so. +
31 And God saw every thing that he had made, and, behold, it was very good. And the evening and the morning were the sixth day. +

+ +
+
+
+
+
+ +
+
+
+

 printer  +  arrowup  + +  arrowright 

+ +
+
+
+ + + + + + + + + diff --git a/tests/resources/bibles/wordproject_index.htm b/tests/resources/bibles/wordproject_index.htm new file mode 100644 index 000000000..861ca2dda --- /dev/null +++ b/tests/resources/bibles/wordproject_index.htm @@ -0,0 +1,222 @@ + + + + + The Holy Bible in the English language with audio narration - KJV + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

WordProject

+
+
+
+ + +
+
+ +
+ +facebook + +twitter + +google + +linkin

+
+ +
+ + +
+
+
+ +
+
+
+

Top + +

+ +
+
+
+ + + + + + + +