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 += "
![](" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + ")
";
}
// 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