This commit is contained in:
Phill Ridout 2014-12-07 17:46:46 +00:00
commit 2aff850b64
39 changed files with 455 additions and 280 deletions

View File

@ -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
"""

View File

@ -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)

View File

@ -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']:

View File

@ -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'

View File

@ -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()
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,11 +308,17 @@ 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)
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:
@ -301,9 +329,21 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties):
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)
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,6 +404,7 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties):
self.max_progress = 0
self.finish_button.setVisible(False)
self.application.process_events()
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()
@ -380,6 +431,14 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties):
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,6 +502,23 @@ 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:
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())
self.screens.set_current_display(self.display_combo_box.currentIndex())
# Set Global Theme
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')
@ -455,17 +531,18 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties):
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)
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)
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))
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()):
@ -474,14 +551,9 @@ class FirstTimeForm(QtGui.QWizard, UiFirstTimeWizard, RegistryProperties):
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))
# Set Default Display
if self.display_combo_box.currentIndex() != -1:
Settings().setValue('core/monitor', self.display_combo_box.currentIndex())
self.screens.set_current_display(self.display_combo_box.currentIndex())
# Set Global Theme
if self.theme_combo_box.currentIndex() != -1:
Settings().setValue('themes/global theme', self.theme_combo_box.currentText())
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):
"""

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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.
"""

View File

@ -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
retries = 0
while True:
try:
remote_version = str(urllib.request.urlopen(req, None).read().decode()).strip()
except IOError:
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)
retries = 0
while True:
try:
page = urllib.request.urlopen(req)
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
log.debug('Downloaded URL = %s' % page.geturl())
except urllib.error.URLError:
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:

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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 = $("<li data-icon=\"false\">").append(
$("<a href=\"#\">").attr("value", parseInt(idx, 10)).text(value["title"]));
$("<a href=\"#\">").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 += ("<div style='font-size:smaller;font-weight:normal'>" + slide["notes"] + "</div>");
if (slide["slide_notes"]) {
text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["slide_notes"] + "</div>");
}
text = text.replace(/\n/g, '<br />');
if (slide["img"]) {

View File

@ -114,8 +114,8 @@ window.OpenLP = {
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
}
// use notes if available
if (slide["notes"]) {
text += '<br />' + slide["notes"];
if (slide["slide_notes"]) {
text += '<br />' + slide["slide_notes"];
}
text = text.replace(/\n/g, "<br />");
$("#currentslide").html(text);

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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)
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)
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)
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:
log.warning('Skipping upgrade_3 step of upgrading the song db')
def upgrade_4(session, metadata):
@ -98,6 +111,8 @@ 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)
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),
@ -106,3 +121,5 @@ def upgrade_4(session, metadata):
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')

View File

@ -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,

View File

@ -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']

View File

@ -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']

View File

@ -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']

View File

@ -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')

View File

@ -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')

View File

@ -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"""
<html>
<head><title>This is not a config file</title></head>
<body>Some text</body>
</html>
"""
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')

View File

@ -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()

View File

@ -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()

View File

@ -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')

View File

@ -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": "<?xml version='1.0' encoding='UTF-8'?>\n<song xmlns=\"http://openlyrics.info/namespace/2009/song\" version=\"0.8\" createdIn=\"OpenLP 2.1.0\" modifiedIn=\"OpenLP 2.1.0\" modifiedDate=\"2014-12-03T21:31:35\"><properties><titles><title>Amazing Grace</title></titles><authors><author>John Newton</author></authors></properties><lyrics><verse name=\"v1\"><lines>Amazing Grace! how sweet the sound<br/>That saved a wretch like me;<br/>I once was lost, but now am found,<br/>Was blind, but now I see.</lines></verse><verse name=\"v2\"><lines>\u2019Twas grace that taught my heart to fear,<br/>And grace my fears relieved;<br/>How precious did that grace appear,<br/>The hour I first believed!</lines></verse><verse name=\"v3\"><lines>Through many dangers, toils and snares<br/>I have already come;<br/>\u2019Tis grace that brought me safe thus far,<br/>And grace will lead me home.</lines></verse><verse name=\"v4\"><lines>The Lord has promised good to me,<br/>His word my hope secures;<br/>He will my shield and portion be<br/>As long as life endures.</lines></verse><verse name=\"v5\"><lines>Yes, when this heart and flesh shall fail,<br/>And mortal life shall cease,<br/>I shall possess within the veil<br/>A life of joy and peace.</lines></verse><verse name=\"v6\"><lines>When we\u2019ve been there a thousand years,<br/>Bright shining as the sun,<br/>We\u2019ve no less days to sing God\u2019s praise<br/>Than when we first begun.</lines></verse></lyrics></song>", "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."}]}}]

Binary file not shown.

Binary file not shown.

View File

@ -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