diff --git a/openlp/core/lib/colorbutton.py b/openlp/core/lib/colorbutton.py index 6ebf9ee99..30d2ad394 100644 --- a/openlp/core/lib/colorbutton.py +++ b/openlp/core/lib/colorbutton.py @@ -73,7 +73,7 @@ class ColorButton(QtGui.QPushButton): @color.setter def color(self, color): """ - Property setter to change the imstamce color + Property setter to change the instance color :param color: String representation of a hexidecimal color """ diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 3f7ffc80f..4971cf445 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -302,7 +302,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): lines = text.strip('\n').split('\n') pages.extend(self._paginate_slide(lines, line_end)) break - count =+ 1 + count += 1 else: # Clean up line endings. pages = self._paginate_slide(text.split('\n'), line_end) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 4e7ff032a..a7051b9d9 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -36,6 +36,7 @@ import html import logging import os import uuid +import ntpath from PyQt4 import QtGui @@ -423,8 +424,12 @@ class ServiceItem(RegistryProperties): if 'background_audio' in header: self.background_audio = [] for filename in header['background_audio']: - # Give them real file paths - self.background_audio.append(os.path.join(path, filename)) + # Give them real file paths. + filepath = filename + if path: + # Windows can handle both forward and backward slashes, so we use ntpath to get the basename + filepath = os.path.join(path, ntpath.basename(filename)) + self.background_audio.append(filepath) self.theme_overwritten = header.get('theme_overwritten', False) if self.service_item_type == ServiceItemType.Text: for slide in service_item['serviceitem']['data']: diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py index 251e0657c..99ddcc92d 100644 --- a/openlp/core/ui/aboutdialog.py +++ b/openlp/core/ui/aboutdialog.py @@ -225,10 +225,10 @@ class Ui_AboutDialog(object): '\n' 'Built With\n' ' Python: http://www.python.org/\n' - ' Qt4: http://qt.digia.com/\n' - ' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/' - 'intro\n' - ' Oxygen Icons: http://oxygen-icons.org/\n' + ' Qt4: http://qt.io\n' + ' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro\n' + ' Oxygen Icons: http://techbase.kde.org/Projects/Oxygen/\n' + ' MuPDF: http://www.mupdf.com/\n' '\n' 'Final Credit\n' ' "For God so loved the world that He gave\n' diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 13d7ab0cf..3f1121542 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -37,14 +37,15 @@ import urllib.request import urllib.parse import urllib.error from tempfile import gettempdir -from configparser import ConfigParser +from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \ - translate, clean_button_text + translate, clean_button_text, trace_error_handler from openlp.core.lib import PluginStatus, build_icon -from openlp.core.utils import get_web_page +from openlp.core.lib.ui import critical_error_message_box +from openlp.core.utils import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT from .firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) @@ -89,27 +90,32 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): super(FirstTimeForm, self).__init__(parent) self.setup_ui(self) + def get_next_page_id(self): + """ + Returns the id of the next FirstTimePage to go to based on enabled plugins + """ + # The songs plugin is enabled + if FirstTimePage.Welcome < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked(): + print('Go for songs! %r' % self.songs_check_box.isChecked()) + return FirstTimePage.Songs + # The Bibles plugin is enabled + elif FirstTimePage.Welcome < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked(): + return FirstTimePage.Bibles + elif FirstTimePage.Welcome < self.currentId() < FirstTimePage.Themes: + return FirstTimePage.Themes + else: + return self.currentId() + 1 + def nextId(self): """ Determine the next page in the Wizard to go to. """ self.application.process_events() if self.currentId() == FirstTimePage.Plugins: - if self.has_run_wizard: - self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) - self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) - self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name( - 'presentations').is_active()) - self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) - self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) - self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active()) - self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) - self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) - self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) if not self.web_access: return FirstTimePage.NoInternet else: - return FirstTimePage.Songs + return self.get_next_page_id() elif self.currentId() == FirstTimePage.Progress: return -1 elif self.currentId() == FirstTimePage.NoInternet: @@ -124,7 +130,7 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): self.application.set_normal_cursor() return FirstTimePage.Defaults else: - return self.currentId() + 1 + return self.get_next_page_id() def exec_(self): """ @@ -141,17 +147,23 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): """ self.screens = screens # check to see if we have web access + self.web_access = False self.web = 'http://openlp.org/files/frw/' self.config = ConfigParser() user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() - self.web_access = get_web_page('%s%s' % (self.web, 'download.cfg'), header=('User-Agent', user_agent)) - if self.web_access: - files = self.web_access.read() - self.config.read_string(files.decode()) - self.web = self.config.get('general', 'base url') - self.songs_url = self.web + self.config.get('songs', 'directory') + '/' - self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' - self.themes_url = self.web + self.config.get('themes', 'directory') + '/' + web_config = get_web_page('%s%s' % (self.web, 'download.cfg'), header=('User-Agent', user_agent)) + if web_config: + files = web_config.read() + try: + self.config.read_string(files.decode()) + self.web = self.config.get('general', 'base url') + self.songs_url = self.web + self.config.get('songs', 'directory') + '/' + self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' + self.themes_url = self.web + self.config.get('themes', 'directory') + '/' + self.web_access = True + except (NoSectionError, NoOptionError, MissingSectionHeaderError): + log.debug('A problem occured while parsing the downloaded config file') + trace_error_handler(log) self.update_screen_list_combo() self.was_download_cancelled = False self.theme_screenshot_thread = None @@ -171,6 +183,17 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): self.no_internet_finish_button.setVisible(False) # Check if this is a re-run of the wizard. self.has_run_wizard = Settings().value('core/has run wizard') + if self.has_run_wizard: + self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) + self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) + self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active()) + self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) + self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) + self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active()) + self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) + self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) + self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) + self.application.set_normal_cursor() # Sort out internet access for downloads if self.web_access: songs = self.config.get('songs', 'languages') @@ -200,7 +223,6 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): # Download the theme screenshots. self.theme_screenshot_thread = ThemeScreenshotThread(self) self.theme_screenshot_thread.start() - self.application.set_normal_cursor() def update_screen_list_combo(self): """ @@ -286,24 +308,42 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): def url_get_file(self, url, f_path): """" Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any - point. + point. Returns False on download error. + + :param url: URL to download + :param f_path: Destination file """ block_count = 0 block_size = 4096 - url_file = urllib.request.urlopen(url) - filename = open(f_path, "wb") - # Download until finished or canceled. - while not self.was_download_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) - block_count += 1 - self._download_progress(block_count, block_size) - filename.close() + retries = 0 + while True: + try: + url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + filename = open(f_path, "wb") + # Download until finished or canceled. + while not self.was_download_cancelled: + data = url_file.read(block_size) + if not data: + break + filename.write(data) + block_count += 1 + self._download_progress(block_count, block_size) + filename.close() + except ConnectionError: + 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_download_cancelled: os.remove(f_path) + return True def _build_theme_screenshots(self): """ @@ -322,9 +362,19 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): :param url: The URL of the file we want to download. """ - site = urllib.request.urlopen(url) - meta = site.info() - return int(meta.get("Content-Length")) + retries = 0 + while True: + try: + site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + meta = site.info() + return int(meta.get("Content-Length")) + except ConnectionException: + if retries > CONNECTION_RETRIES: + raise + else: + retries += 1 + time.sleep(0.1) + continue def _download_progress(self, count, block_size): """ @@ -354,32 +404,41 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): self.max_progress = 0 self.finish_button.setVisible(False) self.application.process_events() - # Loop through the songs list and increase for each selected item - for i in range(self.songs_list_widget.count()): - self.application.process_events() - item = self.songs_list_widget.item(i) - if item.checkState() == QtCore.Qt.Checked: - filename = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.songs_url, filename)) - self.max_progress += size - # Loop through the Bibles list and increase for each selected item - iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget) - while iterator.value(): - self.application.process_events() - item = iterator.value() - if item.parent() and item.checkState(0) == QtCore.Qt.Checked: - filename = item.data(0, QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.bibles_url, filename)) - self.max_progress += size - iterator += 1 - # Loop through the themes list and increase for each selected item - for i in range(self.themes_list_widget.count()): - self.application.process_events() - item = self.themes_list_widget.item(i) - if item.checkState() == QtCore.Qt.Checked: - filename = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.themes_url, filename)) - self.max_progress += size + try: + # Loop through the songs list and increase for each selected item + for i in range(self.songs_list_widget.count()): + self.application.process_events() + item = self.songs_list_widget.item(i) + if item.checkState() == QtCore.Qt.Checked: + filename = item.data(QtCore.Qt.UserRole) + size = self._get_file_size('%s%s' % (self.songs_url, filename)) + self.max_progress += size + # Loop through the Bibles list and increase for each selected item + iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget) + while iterator.value(): + self.application.process_events() + item = iterator.value() + if item.parent() and item.checkState(0) == QtCore.Qt.Checked: + filename = item.data(0, QtCore.Qt.UserRole) + size = self._get_file_size('%s%s' % (self.bibles_url, filename)) + self.max_progress += size + iterator += 1 + # Loop through the themes list and increase for each selected item + for i in range(self.themes_list_widget.count()): + self.application.process_events() + item = self.themes_list_widget.item(i) + if item.checkState() == QtCore.Qt.Checked: + filename = item.data(QtCore.Qt.UserRole) + size = self._get_file_size('%s%s' % (self.themes_url, filename)) + self.max_progress += size + except ConnectionError: + trace_error_handler(log) + critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), + translate('OpenLP.FirstTimeWizard', 'There was a connection problem during ' + 'download, so further downloads will be skipped. Try to re-run the ' + 'First Time Wizard later.')) + self.max_progress = 0 + self.web_access = None if self.max_progress: # Add on 2 for plugins status setting plus a "finished" point. self.max_progress += 2 @@ -443,38 +502,11 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): self._set_plugin_status(self.song_usage_check_box, 'songusage/status') self._set_plugin_status(self.alert_check_box, 'alerts/status') if self.web_access: - # Build directories for downloads - songs_destination = os.path.join(gettempdir(), 'openlp') - bibles_destination = AppLocation.get_section_data_path('bibles') - themes_destination = AppLocation.get_section_data_path('themes') - # Download songs - for i in range(self.songs_list_widget.count()): - item = self.songs_list_widget.item(i) - if item.checkState() == QtCore.Qt.Checked: - filename = item.data(QtCore.Qt.UserRole) - self._increment_progress_bar(self.downloading % filename, 0) - self.previous_size = 0 - destination = os.path.join(songs_destination, str(filename)) - self.url_get_file('%s%s' % (self.songs_url, filename), destination) - # Download Bibles - bibles_iterator = QtGui.QTreeWidgetItemIterator( - self.bibles_tree_widget) - while bibles_iterator.value(): - item = bibles_iterator.value() - if item.parent() and item.checkState(0) == QtCore.Qt.Checked: - bible = item.data(0, QtCore.Qt.UserRole) - self._increment_progress_bar(self.downloading % bible, 0) - self.previous_size = 0 - self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible)) - bibles_iterator += 1 - # Download themes - for i in range(self.themes_list_widget.count()): - item = self.themes_list_widget.item(i) - if item.checkState() == QtCore.Qt.Checked: - theme = item.data(QtCore.Qt.UserRole) - self._increment_progress_bar(self.downloading % theme, 0) - self.previous_size = 0 - self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme)) + if not self._download_selected(): + critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), + translate('OpenLP.FirstTimeWizard', 'There was a connection problem while ' + 'downloading, so further downloads will be skipped. Try to re-run ' + 'the First Time Wizard later.')) # Set Default Display if self.display_combo_box.currentIndex() != -1: Settings().setValue('core/monitor', self.display_combo_box.currentIndex()) @@ -483,6 +515,46 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties): if self.theme_combo_box.currentIndex() != -1: Settings().setValue('themes/global theme', self.theme_combo_box.currentText()) + def _download_selected(self): + """ + Download selected songs, bibles and themes. Returns False on download error + """ + # Build directories for downloads + songs_destination = os.path.join(gettempdir(), 'openlp') + bibles_destination = AppLocation.get_section_data_path('bibles') + themes_destination = AppLocation.get_section_data_path('themes') + # Download songs + for i in range(self.songs_list_widget.count()): + item = self.songs_list_widget.item(i) + if item.checkState() == QtCore.Qt.Checked: + filename = item.data(QtCore.Qt.UserRole) + self._increment_progress_bar(self.downloading % filename, 0) + self.previous_size = 0 + destination = os.path.join(songs_destination, str(filename)) + if not self.url_get_file('%s%s' % (self.songs_url, filename), destination): + return False + # Download Bibles + bibles_iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget) + while bibles_iterator.value(): + item = bibles_iterator.value() + if item.parent() and item.checkState(0) == QtCore.Qt.Checked: + bible = item.data(0, QtCore.Qt.UserRole) + self._increment_progress_bar(self.downloading % bible, 0) + self.previous_size = 0 + if not self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible)): + return False + bibles_iterator += 1 + # Download themes + for i in range(self.themes_list_widget.count()): + item = self.themes_list_widget.item(i) + if item.checkState() == QtCore.Qt.Checked: + theme = item.data(QtCore.Qt.UserRole) + self._increment_progress_bar(self.downloading % theme, 0) + self.previous_size = 0 + if not self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme)): + return False + return True + def _set_plugin_status(self, field, tag): """ Set the status of a plugin. diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 35d05620a..d654ac172 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -706,7 +706,10 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): self.active_plugin.toggle_status(PluginStatus.Inactive) # Set global theme and Registry().execute('theme_update_global') + # Load the themes from files self.theme_manager_contents.load_first_time_themes() + # Update the theme widget + self.theme_manager_contents.load_themes() # Check if any Bibles downloaded. If there are, they will be processed. Registry().execute('bibles_load_list', True) self.application.set_normal_cursor() diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index e8b201cf6..3bcbaa4d4 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -747,8 +747,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage 'File is not a valid service.\n The content encoding is not UTF-8.')) continue os_file = ucs_file.replace('/', os.path.sep) - if not os_file.startswith('audio'): - os_file = os.path.split(os_file)[1] + os_file = os.path.basename(os_file) self.log_debug('Extract file: %s' % os_file) zip_info.filename = os_file zip_file.extract(zip_info, self.service_path) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 85fe240d4..c2c00856f 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -141,6 +141,7 @@ class SlideController(DisplayController, RegistryProperties): self.slide_list = {} self.slide_count = 0 self.slide_image = None + self.controller_width = -1 # Layout for holding panel self.panel_layout = QtGui.QVBoxLayout(self.panel) self.panel_layout.setSpacing(0) @@ -331,9 +332,6 @@ class SlideController(DisplayController, RegistryProperties): self.slide_layout.setMargin(0) self.slide_layout.setObjectName('SlideLayout') self.preview_display = Display(self) - self.preview_display.setGeometry(QtCore.QRect(0, 0, 300, 300)) - self.preview_display.screen = {'size': self.preview_display.geometry()} - self.preview_display.setup() self.slide_layout.insertWidget(0, self.preview_display) self.preview_display.hide() # Actual preview screen @@ -382,13 +380,11 @@ class SlideController(DisplayController, RegistryProperties): Registry().register_function('slidecontroller_live_spin_delay', self.receive_spin_delay) self.toolbar.set_widget_visible(LOOP_LIST, False) self.toolbar.set_widget_visible(WIDE_MENU, False) - else: - self.preview_widget.doubleClicked.connect(self.on_preview_add_to_service) - self.toolbar.set_widget_visible(['editSong'], False) - if self.is_live: self.set_live_hot_keys(self) self.__add_actions_to_widget(self.controller) else: + self.preview_widget.doubleClicked.connect(self.on_preview_add_to_service) + self.toolbar.set_widget_visible(['editSong'], False) self.controller.addActions([self.next_item, self.previous_item]) Registry().register_function('slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop) Registry().register_function('slidecontroller_%s_change' % self.type_prefix, self.on_slide_change) @@ -493,6 +489,11 @@ class SlideController(DisplayController, RegistryProperties): """ self.display.setVisible(False) self.media_controller.media_stop(self) + # Stop looping if active + if self.play_slides_loop.isChecked(): + self.on_play_slides_loop(False) + elif self.play_slides_once.isChecked(): + self.on_play_slides_once(False) def toggle_display(self, action): """ @@ -599,7 +600,10 @@ class SlideController(DisplayController, RegistryProperties): self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.screen = {'size': self.preview_display.geometry()} - self.on_controller_size_changed(self.controller.width()) + # Only update controller layout if width has actually changed + if self.controller_width != self.controller.width(): + self.controller_width = self.controller.width() + self.on_controller_size_changed(self.controller_width) def on_controller_size_changed(self, width): """ @@ -618,6 +622,10 @@ class SlideController(DisplayController, RegistryProperties): elif width < used_space - HIDE_MENU_THRESHOLD and not self.hide_menu.isVisible(): self.set_blank_menu(False) self.toolbar.set_widget_visible(NARROW_MENU) + # Fallback to the standard blank toolbar if the hide_menu is not visible. + elif not self.hide_menu.isVisible(): + self.toolbar.set_widget_visible(NARROW_MENU, False) + self.set_blank_menu() def set_blank_menu(self, visible=True): """ @@ -692,7 +700,9 @@ class SlideController(DisplayController, RegistryProperties): self.mediabar.show() self.previous_item.setVisible(not item.is_media()) self.next_item.setVisible(not item.is_media()) - # The layout of the toolbar is size dependent, so make sure it fits + # The layout of the toolbar is size dependent, so make sure it fits. Reset stored controller_width. + if self.is_live: + self.controller_width = -1 self.on_controller_size_changed(self.controller.width()) # Work-around for OS X, hide and then show the toolbar # See bug #791050 diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 611ac2d4d..0c90986e5 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -67,8 +67,8 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed) self.color_button.colorChanged.connect(self.on_color_changed) self.image_color_button.colorChanged.connect(self.on_image_color_changed) - self.gradient_start_button.colorChanged.connect(self.on_gradient_start_button_changed) - self.gradient_end_button.colorChanged.connect(self.on_gradient_end_button_changed) + self.gradient_start_button.colorChanged.connect(self.on_gradient_start_color_changed) + self.gradient_end_button.colorChanged.connect(self.on_gradient_end_color_changed) self.image_browse_button.clicked.connect(self.on_image_browse_button_clicked) self.image_file_edit.editingFinished.connect(self.on_image_file_edit_editing_finished) self.main_color_button.colorChanged.connect(self.on_main_color_changed) @@ -411,13 +411,13 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): """ self.theme.background_border_color = color - def on_gradient_start_button_changed(self, color): + def on_gradient_start_color_changed(self, color): """ Gradient 2 _color button pushed. """ self.theme.background_start_color = color - def on_gradient_end_button_changed(self, color): + def on_gradient_end_color_changed(self, color): """ Gradient 2 _color button pushed. """ diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index dcf25a439..efe005f34 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -35,6 +35,7 @@ import logging import locale import os import re +import time from shutil import which from subprocess import Popen, PIPE import sys @@ -92,6 +93,8 @@ USER_AGENTS = { 'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0' ] } +CONNECTION_TIMEOUT = 30 +CONNECTION_RETRIES = 2 class VersionThread(QtCore.QThread): @@ -251,10 +254,19 @@ def check_latest_version(current_version): req = urllib.request.Request('http://www.openlp.org/files/version.txt') req.add_header('User-Agent', 'OpenLP/%s' % current_version['full']) remote_version = None - try: - remote_version = str(urllib.request.urlopen(req, None).read().decode()).strip() - except IOError: - log.exception('Failed to download the latest OpenLP version file') + retries = 0 + while True: + try: + remote_version = str(urllib.request.urlopen(req, None, + timeout=CONNECTION_TIMEOUT).read().decode()).strip() + except ConnectionException: + if retries > CONNECTION_RETRIES: + log.exception('Failed to download the latest OpenLP version file') + else: + retries += 1 + time.sleep(0.1) + continue + break if remote_version: version_string = remote_version return version_string @@ -390,11 +402,19 @@ def get_web_page(url, header=None, update_openlp=False): req.add_header(header[0], header[1]) page = None log.debug('Downloading URL = %s' % url) - try: - page = urllib.request.urlopen(req) - log.debug('Downloaded URL = %s' % page.geturl()) - except urllib.error.URLError: - log.exception('The web page could not be downloaded') + retries = 0 + while True: + try: + page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT) + log.debug('Downloaded URL = %s' % page.geturl()) + except (urllib.error.URLError, ConnectionError): + if retries > CONNECTION_RETRIES: + log.exception('The web page could not be downloaded') + raise + else: + time.sleep(0.1) + continue + break if not page: return None if update_openlp: diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index e7f1fdd56..76dc75d35 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -159,7 +159,7 @@ class BiblePlugin(Plugin): self.upgrade_wizard = BibleUpgradeForm(self.main_window, self.manager, self) # If the import was not cancelled then reload. if self.upgrade_wizard.exec_(): - self.media_item.reloadBibles() + self.media_item.reload_bibles() def on_bible_import_click(self): if self.media_item: diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index b0d8d0e7d..8470c2765 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -170,6 +170,9 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties): Returns the version name of the Bible. """ version_name = self.get_object(BibleMeta, 'name') + # Fallback to old way of naming + if not version_name: + version_name = self.get_object(BibleMeta, 'Version') self.name = version_name.value if version_name else None return self.name @@ -969,11 +972,15 @@ class OldBibleDB(QtCore.QObject, Manager): """ Returns the version name of the Bible. """ + self.name = None version_name = self.run_sql('SELECT value FROM metadata WHERE key = "name"') if version_name: self.name = version_name[0][0] else: - self.name = None + # Fallback to old way of naming + version_name = self.run_sql('SELECT value FROM metadata WHERE key = "Version"') + if version_name: + self.name = version_name[0][0] return self.name def get_metadata(self): diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index e1d0dc8f0..2550ae928 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -31,6 +31,7 @@ import os import logging from tempfile import NamedTemporaryFile import re +from shutil import which from subprocess import check_output, CalledProcessError, STDOUT from openlp.core.utils import AppLocation @@ -144,17 +145,10 @@ class PdfController(PresentationController): else: DEVNULL = open(os.devnull, 'wb') # First try to find mupdf - try: - self.mudrawbin = check_output(['which', 'mudraw'], - stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n') - except CalledProcessError: - self.mudrawbin = '' + self.mudrawbin = which('mudraw') # if mupdf isn't installed, fallback to ghostscript if not self.mudrawbin: - try: - self.gsbin = check_output(['which', 'gs'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n') - except CalledProcessError: - self.gsbin = '' + self.gsbin = which('gs') # Last option: check if mudraw is placed in OpenLP base folder if not self.mudrawbin and not self.gsbin: application_path = AppLocation.get_directory(AppLocation.AppDir) diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 9f18c1552..2014988b8 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -67,8 +67,12 @@ window.OpenLP = { var ul = $("#service-manager > div[data-role=content] > ul[data-role=listview]"); ul.html(""); $.each(data.results.items, function (idx, value) { + var text = value["title"]; + if (value["notes"]) { + text += ' - ' + value["notes"]; + } var li = $("
  • ").append( - $("").attr("value", parseInt(idx, 10)).text(value["title"])); + $("").attr("value", parseInt(idx, 10)).text(text)); li.attr("uuid", value["id"]) li.children("a").click(OpenLP.setItem); ul.append(li); @@ -98,8 +102,8 @@ window.OpenLP = { } else { text += slide["text"]; } - if (slide["notes"]) { - text += ("
    " + slide["notes"] + "
    "); + if (slide["slide_notes"]) { + text += ("
    " + slide["slide_notes"] + "
    "); } text = text.replace(/\n/g, '
    '); if (slide["img"]) { diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index e63025b80..4834b4664 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -114,8 +114,8 @@ window.OpenLP = { text += "

    "; } // use notes if available - if (slide["notes"]) { - text += '
    ' + slide["notes"]; + if (slide["slide_notes"]) { + text += '
    ' + slide["slide_notes"]; } text = text.replace(/\n/g, "
    "); $("#currentslide").html(text); diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index 22e2495c0..1313c9f9c 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -521,7 +521,7 @@ class HttpRouter(RegistryProperties): if current_item.is_capable(ItemCapabilities.HasDisplayTitle): item['title'] = str(frame['display_title']) if current_item.is_capable(ItemCapabilities.HasNotes): - item['notes'] = str(frame['notes']) + item['slide_notes'] = str(frame['notes']) if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ Settings().value('remotes/thumbnails'): # If the file is under our app directory tree send the portion after the match @@ -531,8 +531,6 @@ class HttpRouter(RegistryProperties): item['text'] = str(frame['title']) item['html'] = str(frame['title']) item['selected'] = (self.live_controller.selected_row == index) - if current_item.notes: - item['notes'] = item.get('notes', '') + '\n' + current_item.notes data.append(item) json_data = {'results': {'slides': data}} if current_item: diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index 1a27e8d69..f3d0a74dc 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -217,4 +217,5 @@ class OpenLPSongImport(SongImport): self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % new_song.title) if self.stop_import_flag: break + self.source_session.close() engine.dispose() diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 18d964115..7426b032c 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -395,6 +395,18 @@ class SongMediaItem(MediaManagerItem): new_song = self.open_lyrics.xml_to_song(song_xml) new_song.title = '%s <%s>' % \ (new_song.title, translate('SongsPlugin.MediaItem', 'copy', 'For song cloning')) + # Copy audio files from the old to the new song + if len(old_song.media_files) > 0: + save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(new_song.id)) + check_directory_exists(save_path) + for media_file in old_song.media_files: + new_media_file_name = os.path.join(save_path, os.path.basename(media_file.file_name)) + shutil.copyfile(media_file.file_name, new_media_file_name) + new_media_file = MediaFile() + new_media_file.file_name = new_media_file_name + new_media_file.type = media_file.type + new_media_file.weight = media_file.weight + new_song.media_files.append(new_media_file) self.plugin.manager.save_object(new_song) self.on_song_list_load() diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py index 5b7255266..b8945f8a9 100644 --- a/openlp/plugins/songs/lib/upgrade.py +++ b/openlp/plugins/songs/lib/upgrade.py @@ -32,10 +32,11 @@ backend for the Songs plugin """ import logging -from sqlalchemy import Column, ForeignKey, types +from sqlalchemy import Table, Column, ForeignKey, types from sqlalchemy.sql.expression import func, false, null, text from openlp.core.lib.db import get_upgrade_op +from openlp.core.common import trace_error_handler log = logging.getLogger(__name__) __version__ = 4 @@ -57,12 +58,16 @@ def upgrade_1(session, metadata): :param metadata: """ op = get_upgrade_op(session) - op.drop_table('media_files_songs') - op.add_column('media_files', Column('song_id', types.Integer(), server_default=null())) - op.add_column('media_files', Column('weight', types.Integer(), server_default=text('0'))) - if metadata.bind.url.get_dialect().name != 'sqlite': - # SQLite doesn't support ALTER TABLE ADD CONSTRAINT - op.create_foreign_key('fk_media_files_song_id', 'media_files', 'songs', ['song_id', 'id']) + songs_table = Table('songs', metadata, autoload=True) + if 'media_files_songs' in [t.name for t in metadata.tables.values()]: + op.drop_table('media_files_songs') + op.add_column('media_files', Column('song_id', types.Integer(), server_default=null())) + op.add_column('media_files', Column('weight', types.Integer(), server_default=text('0'))) + if metadata.bind.url.get_dialect().name != 'sqlite': + # SQLite doesn't support ALTER TABLE ADD CONSTRAINT + op.create_foreign_key('fk_media_files_song_id', 'media_files', 'songs', ['song_id', 'id']) + else: + log.warning('Skipping upgrade_1 step of upgrading the song db') def upgrade_2(session, metadata): @@ -72,8 +77,12 @@ def upgrade_2(session, metadata): This upgrade adds a create_date and last_modified date to the songs table """ op = get_upgrade_op(session) - op.add_column('songs', Column('create_date', types.DateTime(), default=func.now())) - op.add_column('songs', Column('last_modified', types.DateTime(), default=func.now())) + songs_table = Table('songs', metadata, autoload=True) + if 'create_date' not in [col.name for col in songs_table.c.values()]: + op.add_column('songs', Column('create_date', types.DateTime(), default=func.now())) + op.add_column('songs', Column('last_modified', types.DateTime(), default=func.now())) + else: + log.warning('Skipping upgrade_2 step of upgrading the song db') def upgrade_3(session, metadata): @@ -83,10 +92,14 @@ def upgrade_3(session, metadata): This upgrade adds a temporary song flag to the songs table """ op = get_upgrade_op(session) - if metadata.bind.url.get_dialect().name == 'sqlite': - op.add_column('songs', Column('temporary', types.Boolean(create_constraint=False), server_default=false())) + songs_table = Table('songs', metadata, autoload=True) + if 'temporary' not in [col.name for col in songs_table.c.values()]: + if metadata.bind.url.get_dialect().name == 'sqlite': + op.add_column('songs', Column('temporary', types.Boolean(create_constraint=False), server_default=false())) + else: + op.add_column('songs', Column('temporary', types.Boolean(), server_default=false())) else: - op.add_column('songs', Column('temporary', types.Boolean(), server_default=false())) + log.warning('Skipping upgrade_3 step of upgrading the song db') def upgrade_4(session, metadata): @@ -98,11 +111,15 @@ def upgrade_4(session, metadata): # Since SQLite doesn't support changing the primary key of a table, we need to recreate the table # and copy the old values op = get_upgrade_op(session) - op.create_table('authors_songs_tmp', - Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True), - Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True), - Column('author_type', types.String(), primary_key=True, - nullable=False, server_default=text('""'))) - op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs') - op.drop_table('authors_songs') - op.rename_table('authors_songs_tmp', 'authors_songs') + songs_table = Table('songs', metadata) + if 'author_type' not in [col.name for col in songs_table.c.values()]: + op.create_table('authors_songs_tmp', + Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True), + Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True), + Column('author_type', types.String(), primary_key=True, + nullable=False, server_default=text('""'))) + op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs') + op.drop_table('authors_songs') + op.rename_table('authors_songs_tmp', 'authors_songs') + else: + log.warning('Skipping upgrade_4 step of upgrading the song db') diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index b7daebaab..be04db9cc 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -60,7 +60,7 @@ __default_settings__ = { 'songs/last search type': SongSearch.Entire, 'songs/last import type': SongFormat.OpenLyrics, 'songs/update service on edit': False, - 'songs/search as type': False, + 'songs/search as type': True, 'songs/add song from service': True, 'songs/display songbar': True, 'songs/display songbook': False, diff --git a/resources/pyinstaller/hook-openlp.core.ui.media.py b/resources/pyinstaller/hook-openlp.core.ui.media.py deleted file mode 100644 index a17856dca..000000000 --- a/resources/pyinstaller/hook-openlp.core.ui.media.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2014 Raoul Snyman # -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# 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 # -############################################################################### - -hiddenimports = ['openlp.core.ui.media.phononplayer', - 'openlp.core.ui.media.vlcplayer', - 'openlp.core.ui.media.webkitplayer'] diff --git a/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py b/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py deleted file mode 100644 index 6ffb416fa..000000000 --- a/resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2014 Raoul Snyman # -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# 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 # -############################################################################### - -hiddenimports = ['openlp.plugins.presentations.lib.impresscontroller', - 'openlp.plugins.presentations.lib.powerpointcontroller', - 'openlp.plugins.presentations.lib.pptviewcontroller', - 'openlp.plugins.presentations.lib.pdfcontroller'] diff --git a/resources/pyinstaller/hook-openlp.py b/resources/pyinstaller/hook-openlp.py deleted file mode 100644 index c45dec009..000000000 --- a/resources/pyinstaller/hook-openlp.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2014 Raoul Snyman # -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# 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 # -############################################################################### - -hiddenimports = ['plugins.songs.songsplugin', - 'plugins.bibles.bibleplugin', - 'plugins.presentations.presentationplugin', - 'plugins.media.mediaplugin', - 'plugins.images.imageplugin', - 'plugins.custom.customplugin', - 'plugins.songusage.songusageplugin', - 'plugins.remotes.remoteplugin', - 'plugins.alerts.alertsplugin'] diff --git a/tests/functional/openlp_core_lib/test_color_button.py b/tests/functional/openlp_core_lib/test_color_button.py index a7b743918..3d605b6d5 100644 --- a/tests/functional/openlp_core_lib/test_color_button.py +++ b/tests/functional/openlp_core_lib/test_color_button.py @@ -202,5 +202,5 @@ class TestColorDialog(TestCase): widget.on_clicked() # THEN: change_color should have been called and the colorChanged signal should have been emitted - self.mocked_change_color.assert_call_once_with('#ffffff') + self.mocked_change_color.assert_called_once_with('#ffffff') self.mocked_color_changed.emit.assert_called_once_with('#ffffff') diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index 629b17114..8b7075d7c 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -46,7 +46,7 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n'\ '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\ 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456'] -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service')) class TestServiceItem(TestCase): @@ -271,3 +271,36 @@ class TestServiceItem(TestCase): self.assertEqual(service_item.start_time, 654.375, 'Start time should be 654.375') self.assertEqual(service_item.end_time, 672.069, 'End time should be 672.069') self.assertEqual(service_item.media_length, 17.694, 'Media length should be 17.694') + + def service_item_load_song_and_audio_from_service_test(self): + """ + Test the Service Item - adding a song slide from a saved service + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: We add a custom from a saved service + line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj') + service_item.set_from_service(line, '/test/') + + # THEN: We should get back a valid service item + self.assertTrue(service_item.is_valid, 'The new service item should be valid') + assert_length(0, service_item._display_frames, 'The service item should have no display frames') + assert_length(7, service_item.capabilities, 'There should be 7 default custom item capabilities') + + # WHEN: We render the frames of the service item + service_item.render(True) + + # THEN: The frames should also be valid + self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"') + self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'], + 'The returned text matches the input, except the last line feed') + self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1), + 'The first line has been returned') + self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0), + '"Amazing Grace! how sweet the s" has been returned as the title') + self.assertEqual('’Twas grace that taught my hea', service_item.get_frame_title(1), + '"’Twas grace that taught my hea" has been returned as the title') + self.assertEqual('/test/amazing_grace.mp3', service_item.background_audio[0], + '"/test/amazing_grace.mp3" should be in the background_audio list') diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index ed7a7a9e8..ead9ef4cf 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -49,6 +49,22 @@ directory = bibles directory = themes """ +FAKE_BROKEN_CONFIG = b""" +[general] +base url = http://example.com/frw/ +[songs] +directory = songs +[bibles] +directory = bibles +""" + +FAKE_INVALID_CONFIG = b""" + +This is not a config file +Some text + +""" + class TestFirstTimeForm(TestCase, TestMixin): @@ -104,3 +120,33 @@ class TestFirstTimeForm(TestCase, TestMixin): self.assertEqual(expected_songs_url, first_time_form.songs_url, 'The songs URL should be correct') self.assertEqual(expected_bibles_url, first_time_form.bibles_url, 'The bibles URL should be correct') self.assertEqual(expected_themes_url, first_time_form.themes_url, 'The themes URL should be correct') + + def broken_config_test(self): + """ + Test if we can handle an config file with missing data + """ + # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked broken config file + with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: + first_time_form = FirstTimeForm(None) + mocked_get_web_page.return_value.read.return_value = FAKE_BROKEN_CONFIG + + # WHEN: The First Time Wizard is initialised + first_time_form.initialize(MagicMock()) + + # THEN: The First Time Form should not have web access + self.assertFalse(first_time_form.web_access, 'There should not be web access with a broken config file') + + def invalid_config_test(self): + """ + Test if we can handle an config file in invalid format + """ + # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked invalid config file + with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: + first_time_form = FirstTimeForm(None) + mocked_get_web_page.return_value.read.return_value = FAKE_INVALID_CONFIG + + # WHEN: The First Time Wizard is initialised + first_time_form.initialize(MagicMock()) + + # THEN: The First Time Form should not have web access + self.assertFalse(first_time_form.web_access, 'There should not be web access with an invalid config file') diff --git a/tests/functional/openlp_core_ui/test_slidecontroller.py b/tests/functional/openlp_core_ui/test_slidecontroller.py index 1d241a317..d8663c20c 100644 --- a/tests/functional/openlp_core_ui/test_slidecontroller.py +++ b/tests/functional/openlp_core_ui/test_slidecontroller.py @@ -225,6 +225,10 @@ class TestSlideController(TestCase): Registry().register('media_controller', mocked_media_controller) slide_controller = SlideController(None) slide_controller.display = mocked_display + play_slides = MagicMock() + play_slides.isChecked.return_value = False + slide_controller.play_slides_loop = play_slides + slide_controller.play_slides_once = play_slides # WHEN: live_escape() is called slide_controller.live_escape() diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 075ecb14f..de3e68791 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -335,7 +335,7 @@ class TestUtils(TestCase): self.assertEqual(1, mocked_request_object.add_header.call_count, 'There should only be 1 call to add_header') mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object) + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) mocked_page_object.geturl.assert_called_with() self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called') self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') @@ -365,7 +365,7 @@ class TestUtils(TestCase): self.assertEqual(2, mocked_request_object.add_header.call_count, 'There should only be 2 calls to add_header') mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object) + 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') @@ -393,7 +393,7 @@ class TestUtils(TestCase): self.assertEqual(1, mocked_request_object.add_header.call_count, 'There should only be 1 call to add_header') self.assertEqual(0, mock_get_user_agent.call_count, '_get_user_agent should not have been called') - mock_urlopen.assert_called_with(mocked_request_object) + 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') @@ -425,7 +425,7 @@ class TestUtils(TestCase): mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') self.assertEqual(1, mocked_request_object.add_header.call_count, 'There should only be 1 call to add_header') - mock_urlopen.assert_called_with(mocked_request_object) + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) mocked_page_object.geturl.assert_called_with() mocked_registry_object.get.assert_called_with('application') mocked_application_object.process_events.assert_called_with() diff --git a/tests/functional/openlp_plugins/songs/test_db.py b/tests/functional/openlp_plugins/songs/test_db.py index e696ea94b..690b89a18 100644 --- a/tests/functional/openlp_plugins/songs/test_db.py +++ b/tests/functional/openlp_plugins/songs/test_db.py @@ -29,9 +29,15 @@ """ This module contains tests for the db submodule of the Songs plugin. """ +import os +import shutil from unittest import TestCase +from tempfile import mkdtemp from openlp.plugins.songs.lib.db import Song, Author, AuthorType +from openlp.plugins.songs.lib import upgrade +from openlp.core.lib.db import upgrade_db +from tests.utils.constants import TEST_RESOURCES_PATH class TestDB(TestCase): @@ -39,6 +45,18 @@ class TestDB(TestCase): Test the functions in the :mod:`db` module. """ + def setUp(self): + """ + Setup for tests + """ + self.tmp_folder = mkdtemp() + + def tearDown(self): + """ + Clean up after tests + """ + shutil.rmtree(self.tmp_folder) + def test_add_author(self): """ Test adding an author to a song @@ -153,3 +171,37 @@ class TestDB(TestCase): # THEN: It should return the name with the type in brackets self.assertEqual("John Doe (Words)", display_name) + + def test_upgrade_old_song_db(self): + """ + Test that we can upgrade an old song db to the current schema + """ + # GIVEN: An old song db + old_db_path = os.path.join(TEST_RESOURCES_PATH, "songs", 'songs-1.9.7.sqlite') + old_db_tmp_path = os.path.join(self.tmp_folder, 'songs-1.9.7.sqlite') + shutil.copyfile(old_db_path, old_db_tmp_path) + db_url = 'sqlite:///' + old_db_tmp_path + + # WHEN: upgrading the db + updated_to_version, latest_version = upgrade_db(db_url, upgrade) + + # Then the song db should have been upgraded to the latest version + self.assertEqual(updated_to_version, latest_version, + 'The song DB should have been upgrade to the latest version') + + def test_upgrade_invalid_song_db(self): + """ + Test that we can upgrade an invalid song db to the current schema + """ + # GIVEN: A song db with invalid version + invalid_db_path = os.path.join(TEST_RESOURCES_PATH, "songs", 'songs-2.2-invalid.sqlite') + invalid_db_tmp_path = os.path.join(self.tmp_folder, 'songs-2.2-invalid.sqlite') + shutil.copyfile(invalid_db_path, invalid_db_tmp_path) + db_url = 'sqlite:///' + invalid_db_tmp_path + + # WHEN: upgrading the db + updated_to_version, latest_version = upgrade_db(db_url, upgrade) + + # Then the song db should have been upgraded to the latest version without errors + self.assertEqual(updated_to_version, latest_version, + 'The song DB should have been upgrade to the latest version') diff --git a/tests/resources/migrate_video_20_22.osd b/tests/resources/service/migrate_video_20_22.osd similarity index 100% rename from tests/resources/migrate_video_20_22.osd rename to tests/resources/service/migrate_video_20_22.osd diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/service/serviceitem-dvd.osj similarity index 100% rename from tests/resources/serviceitem-dvd.osj rename to tests/resources/service/serviceitem-dvd.osj diff --git a/tests/resources/service/serviceitem-song-linked-audio.osj b/tests/resources/service/serviceitem-song-linked-audio.osj new file mode 100644 index 000000000..e361a8757 --- /dev/null +++ b/tests/resources/service/serviceitem-song-linked-audio.osj @@ -0,0 +1 @@ +[{"serviceitem": {"header": {"will_auto_start": false, "title": "Amazing Grace", "audit": ["Amazing Grace", ["John Newton"], "", ""], "processor": null, "theme_overwritten": false, "start_time": 0, "auto_play_slides_loop": false, "plugin": "songs", "auto_play_slides_once": false, "from_plugin": false, "media_length": 0, "xml_version": "\nAmazing GraceJohn NewtonAmazing Grace! how sweet the sound
    That saved a wretch like me;
    I once was lost, but now am found,
    Was blind, but now I see.
    \u2019Twas grace that taught my heart to fear,
    And grace my fears relieved;
    How precious did that grace appear,
    The hour I first believed!
    Through many dangers, toils and snares
    I have already come;
    \u2019Tis grace that brought me safe thus far,
    And grace will lead me home.
    The Lord has promised good to me,
    His word my hope secures;
    He will my shield and portion be
    As long as life endures.
    Yes, when this heart and flesh shall fail,
    And mortal life shall cease,
    I shall possess within the veil
    A life of joy and peace.
    When we\u2019ve been there a thousand years,
    Bright shining as the sun,
    We\u2019ve no less days to sing God\u2019s praise
    Than when we first begun.
    ", "timed_slide_interval": 0, "data": {"title": "amazing grace@", "authors": "John Newton"}, "type": 1, "background_audio": ["/home/tgc/.local/share/openlp/songs/audio/7/amazing_grace.mp3"], "theme": null, "footer": ["Amazing Grace", "Written by: John Newton"], "name": "songs", "capabilities": [2, 1, 5, 8, 9, 13, 15], "end_time": 0, "notes": "", "search": "", "icon": ":/plugins/plugin_songs.png"}, "data": [{"title": "Amazing Grace! how sweet the s", "verseTag": "V1", "raw_slide": "Amazing Grace! how sweet the sound\nThat saved a wretch like me;\nI once was lost, but now am found,\nWas blind, but now I see."}, {"title": "\u2019Twas grace that taught my hea", "verseTag": "V2", "raw_slide": "\u2019Twas grace that taught my heart to fear,\nAnd grace my fears relieved;\nHow precious did that grace appear,\nThe hour I first believed!"}, {"title": "Through many dangers, toils an", "verseTag": "V3", "raw_slide": "Through many dangers, toils and snares\nI have already come;\n\u2019Tis grace that brought me safe thus far,\nAnd grace will lead me home."}, {"title": "The Lord has promised good to ", "verseTag": "V4", "raw_slide": "The Lord has promised good to me,\nHis word my hope secures;\nHe will my shield and portion be\nAs long as life endures."}, {"title": "Yes, when this heart and flesh", "verseTag": "V5", "raw_slide": "Yes, when this heart and flesh shall fail,\nAnd mortal life shall cease,\nI shall possess within the veil\nA life of joy and peace."}, {"title": "When we\u2019ve been there a thousa", "verseTag": "V6", "raw_slide": "When we\u2019ve been there a thousand years,\nBright shining as the sun,\nWe\u2019ve no less days to sing God\u2019s praise\nThan when we first begun."}]}}] diff --git a/tests/resources/serviceitem_custom_1.osj b/tests/resources/service/serviceitem_custom_1.osj similarity index 100% rename from tests/resources/serviceitem_custom_1.osj rename to tests/resources/service/serviceitem_custom_1.osj diff --git a/tests/resources/serviceitem_image_1.osj b/tests/resources/service/serviceitem_image_1.osj similarity index 100% rename from tests/resources/serviceitem_image_1.osj rename to tests/resources/service/serviceitem_image_1.osj diff --git a/tests/resources/serviceitem_image_2.osj b/tests/resources/service/serviceitem_image_2.osj similarity index 100% rename from tests/resources/serviceitem_image_2.osj rename to tests/resources/service/serviceitem_image_2.osj diff --git a/tests/resources/serviceitem_image_3.osj b/tests/resources/service/serviceitem_image_3.osj similarity index 100% rename from tests/resources/serviceitem_image_3.osj rename to tests/resources/service/serviceitem_image_3.osj diff --git a/tests/resources/songs/songs-1.9.7.sqlite b/tests/resources/songs/songs-1.9.7.sqlite new file mode 100644 index 000000000..98505464b Binary files /dev/null and b/tests/resources/songs/songs-1.9.7.sqlite differ diff --git a/tests/resources/songs/songs-2.2-invalid.sqlite b/tests/resources/songs/songs-2.2-invalid.sqlite new file mode 100644 index 000000000..0c991d5a3 Binary files /dev/null and b/tests/resources/songs/songs-2.2-invalid.sqlite differ diff --git a/tests/utils/osdinteraction.py b/tests/utils/osdinteraction.py index 8d12943e8..4b016d1ae 100644 --- a/tests/utils/osdinteraction.py +++ b/tests/utils/osdinteraction.py @@ -42,7 +42,7 @@ def read_service_from_file(file_name): @param file_name: File name of an OSD file residing in the tests/resources folder. @return: The service contained in the file. """ - service_file = os.path.join(TEST_RESOURCES_PATH, file_name) + service_file = os.path.join(TEST_RESOURCES_PATH, 'service', file_name) with open(service_file, 'r') as open_file: service = json.load(open_file) return service