This commit is contained in:
Mateus Meyer Jiacomelli 2022-11-26 23:34:34 -03:00
commit 0574505ca3
42 changed files with 1077 additions and 135 deletions

View File

@ -11,6 +11,7 @@ indent_style = space
[*.py]
indent_size = 4
continuation_indent_size = 8
max_line_length = 120
[*.{html,js,json,yaml,yml}]

View File

@ -60,7 +60,7 @@ after_test:
$ErrorActionPreference = "Continue"
# This is where we create a package using PyInstaller
# Install PyInstaller
python -m pip install --no-warn-script-location pyinstaller
python -m pip install --no-warn-script-location pyinstaller==4.9
# Some windows only stuff...
If ($isWindows) {
# Disabled portable installers - can't figure out how to make them silent

View File

@ -78,9 +78,6 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin):
"""
super(HttpServer, self).__init__(parent)
Registry().register('authentication_token', token_hex())
if not Registry().get_flag('no_web_server'):
worker = HttpWorker()
run_thread(worker, 'http_server')
def bootstrap_post_set_up(self):
"""
@ -89,3 +86,6 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin):
create_paths(AppLocation.get_section_data_path('remotes'))
self.poller = Poller()
Registry().register('poller', self.poller)
if not Registry().get_flag('no_web_server'):
worker = HttpWorker()
run_thread(worker, 'http_server')

View File

@ -40,6 +40,7 @@ def controller_live_items():
if current_item:
live_item = current_item.to_dict()
live_item['slides'][live_controller.selected_row]['selected'] = True
live_item['id'] = str(current_item.unique_identifier)
return jsonify(live_item)
@ -51,6 +52,7 @@ def controller_live_item():
live_item = {}
if current_item:
live_item = current_item.to_dict(True, live_controller.selected_row)
live_item['id'] = str(current_item.unique_identifier)
return jsonify(live_item)

View File

@ -21,11 +21,13 @@
##########################################################################
import logging
import re
from flask import abort, request, Blueprint, jsonify
from openlp.core.api.lib import login_required
from openlp.core.lib.plugin import PluginStatus
from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib import transpose_lyrics
log = logging.getLogger(__name__)
@ -134,3 +136,45 @@ def set_search_option(plugin):
else:
log.error('Invalid option or value')
return '', 400
@plugins.route('/songs/transpose-live-item/<transpose_value>', methods=['GET'])
@login_required
def transpose(transpose_value):
log.debug('songs/transpose-live-item called')
if transpose_value:
try:
transpose_value = int(transpose_value)
except ValueError:
abort(400)
# Get lyrics from the live serviceitem in the live-controller and transpose it
live_controller = Registry().get('live_controller')
current_item = live_controller.service_item
# make sure an service item is currently displayed and that it is a song
if not current_item or current_item.name != 'songs':
abort(400)
previous_pages = {}
chord_song_text = ''
# re-create the song lyrics with OpenLP verse-tags to be able to transpose in one go so any keys are used
for raw_slide in current_item.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
pages = current_item.renderer.format_slide(raw_slide['text'], current_item)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
chord_song_text += '---[Verse:{verse_tag}]---\n'.format(verse_tag=verse_tag)
chord_song_text += page
chord_song_text += '\n'
# transpose
transposed_lyrics = transpose_lyrics(chord_song_text, transpose_value)
# re-split into verses
chord_slides = []
verse_list = re.split(r'---\[Verse:(.+?)\]---', transposed_lyrics)
# remove first blank entry
verse_list = verse_list[1:]
for i in range(0, len(verse_list), 2):
chord_slides.append({'chords': verse_list[i + 1].strip(), 'verse': verse_list[i]})
return jsonify(chord_slides), 200
abort(400)

View File

@ -33,7 +33,7 @@ import time
from websockets import serve
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.threading import ThreadWorker, run_thread
from openlp.core.api.websocketspoll import WebSocketPoller
@ -165,7 +165,7 @@ class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin):
self.event_loop.call_soon_threadsafe(queue.put_nowait, state)
class WebSocketServer(RegistryProperties, QtCore.QObject, LogMixin):
class WebSocketServer(RegistryBase, RegistryProperties, QtCore.QObject, LogMixin):
"""
Wrapper round a server instance
"""
@ -176,6 +176,9 @@ class WebSocketServer(RegistryProperties, QtCore.QObject, LogMixin):
super(WebSocketServer, self).__init__()
self.worker = None
def bootstrap_post_set_up(self):
self.start()
def start(self):
"""
Starts the WebSockets server

View File

@ -365,6 +365,7 @@ class Settings(QtCore.QSettings):
'themes/theme level': ThemeLevel.Global,
'themes/item transitions': False,
'themes/hot reload': False,
'user interface/is preset layout': False,
'user interface/live panel': True,
'user interface/live splitter geometry': QtCore.QByteArray(),
'user interface/lock panel': True,
@ -374,8 +375,11 @@ class Settings(QtCore.QSettings):
'user interface/main window state': QtCore.QByteArray(),
'user interface/preview panel': True,
'user interface/preview splitter geometry': QtCore.QByteArray(),
'user interface/is preset layout': False,
'user interface/theme manager view mode': 1,
'user interface/show library': True,
'user interface/show projectors': True,
'user interface/show service': True,
'user interface/show themes': True,
'projector/show after wizard': False,
'projector/db type': 'sqlite',
'projector/db username': '',

View File

@ -815,7 +815,8 @@ class ThemePreviewRenderer(DisplayWindow, LogMixin):
check_string = separator.join(html_list[index + 1:]).strip()
if self._text_fits_on_slide(html_tags + check_string):
previous_html = html_tags + check_string + line_end
previous_raw = raw_tags + check_string + line_end
check_string_raw = separator.join(raw_list[index + 1:]).strip()
previous_raw = raw_tags + check_string_raw + line_end
break
else:
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.

View File

@ -32,7 +32,6 @@ from openlp.core.common import Singleton
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
log = logging.getLogger(__name__)
@ -40,6 +39,7 @@ class Screen(object):
"""
A Python representation of a screen
"""
def __init__(self, number=None, geometry=None, custom_geometry=None, is_primary=False, is_display=False):
"""
Set up the screen object
@ -241,6 +241,10 @@ class ScreenList(metaclass=Singleton):
If more than 1 screen, set first non-primary screen to display, otherwise just set the available screen as
display.
"""
# Reset first, so we end up with just one display at most
for screen in self:
screen.is_display = False
if len(self) > 1:
for screen in self:
if not screen.is_primary:
@ -349,6 +353,7 @@ class ScreenList(metaclass=Singleton):
"""
Update the list of screens
"""
def _screen_compare(this, other):
"""
Compare screens. Can't use a key here because of the nested property and method to be called
@ -364,6 +369,7 @@ class ScreenList(metaclass=Singleton):
return 1
else:
return 0
self.screens = []
os_screens = self.application.screens()
os_screens.sort(key=cmp_to_key(_screen_compare))
@ -379,8 +385,16 @@ class ScreenList(metaclass=Singleton):
:param changed_screen: The screen which has been plugged.
"""
number = len(self.screens)
# Ensure we have only one primary screen in the list.
is_primary = self.application.primaryScreen() == changed_screen
if is_primary:
for screen in self.screens:
screen.is_primary = False
self.screens.append(Screen(number, changed_screen.geometry(),
is_primary=self.application.primaryScreen() == changed_screen))
self.find_new_display_screen()
changed_screen.geometryChanged.connect(self.screens[-1].on_geometry_changed)
Registry().execute('config_screen_changed')
@ -411,4 +425,6 @@ class ScreenList(metaclass=Singleton):
"""
for screen in self.screens:
screen.is_primary = self.application.primaryScreen().geometry() == screen.geometry
self.find_new_display_screen()
Registry().execute('config_screen_changed')

View File

@ -77,6 +77,7 @@ class WebEngineView(QtWebEngineWidgets.QWebEngineView):
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalStorageEnabled, True)
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessFileUrls, True)
self.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
def eventFilter(self, obj, ev):
"""

View File

@ -224,7 +224,7 @@ class ServiceItem(RegistryProperties):
rendered_slide = {
'title': raw_slide['title'],
'text': render_tags(page),
'chords': render_tags(page, can_render_chords=True),
'chords': page,
'verse': index,
'footer': self.footer_html
}
@ -379,7 +379,7 @@ class ServiceItem(RegistryProperties):
'start_time': self.start_time,
'end_time': self.end_time,
'media_length': self.media_length,
'background_audio': self.background_audio,
'background_audio': [],
'theme_overwritten': self.theme_overwritten,
'will_auto_start': self.will_auto_start,
'processor': self.processor,
@ -387,6 +387,14 @@ class ServiceItem(RegistryProperties):
'sha256_file_hash': self.sha256_file_hash,
'stored_filename': stored_filename
}
for file_path, file_hash in self.background_audio:
if lite_save:
path_str = str(file_path)
else:
# a side-effect of this is that if the song is imported from the service created from this, the
# background audio filename shown in the ui will be the file_hash + the suffix.
path_str = '{hash}{suffix}'.format(hash=file_hash, suffix=file_path.suffix)
service_header['background_audio'].append(path_str)
service_data = []
if self.service_item_type == ServiceItemType.Text:
for slide in self.slides:
@ -481,7 +489,9 @@ class ServiceItem(RegistryProperties):
self.stored_filename = header.get('stored_filename', None)
if 'background_audio' in header and State().check_preconditions('media'):
self.background_audio = []
for file_path in header['background_audio']:
for file_str in header['background_audio']:
file_path = Path(file_str)
file_hash = None
# In OpenLP 3.0 we switched to storing Path objects in JSON files
if version >= 3:
if path:
@ -490,7 +500,9 @@ class ServiceItem(RegistryProperties):
# Handle service files prior to OpenLP 3.0
# Windows can handle both forward and backward slashes, so we use ntpath to get the basename
file_path = path / ntpath.basename(file_path)
self.background_audio.append(file_path)
if not file_hash:
file_hash = sha256_file_hash(file_path)
self.background_audio.append((file_path, file_hash))
self.theme_overwritten = header.get('theme_overwritten', False)
if self.service_item_type == ServiceItemType.Text:
for slide in service_item['serviceitem']['data']:
@ -887,7 +899,7 @@ class ServiceItem(RegistryProperties):
'data': self.data_string or {},
'fromPlugin': self.from_plugin,
'capabilities': self.capabilities,
'backgroundAudio': [str(file_path) for file_path in self.background_audio],
'backgroundAudio': self.background_audio,
'isThemeOverwritten': self.theme_overwritten,
'slides': []
}

View File

@ -277,7 +277,7 @@ class Ui_MainWindow(object):
self.settings_configure_item = create_action(main_window, 'settingsConfigureItem',
icon=UiIcons().settings, can_shortcuts=True,
category=UiStrings().Settings)
# Give QT Extra Hint that this is the Preferences Menu Item
# Give Qt Extra Hint that this is the Preferences Menu Item
self.settings_configure_item.setMenuRole(QtWidgets.QAction.PreferencesRole)
self.settings_import_item = create_action(main_window, 'settingsImportItem',
category=UiStrings().Import, can_shortcuts=True)
@ -287,7 +287,7 @@ class Ui_MainWindow(object):
self.about_item = create_action(main_window, 'aboutItem', icon=UiIcons().info,
can_shortcuts=True, category=UiStrings().Help,
triggers=self.on_about_item_clicked)
# Give QT Extra Hint that this is an About Menu Item
# Give Qt Extra Hint that this is an About Menu Item
self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
if is_win():
self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / 'OpenLP.chm'
@ -480,9 +480,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.copy_data = False
self.settings.set_up_default_values()
self.about_form = AboutForm(self)
self.ws_server = WebSocketServer()
self.ws_server.start()
self.http_server = HttpServer(self)
SettingsForm(self)
self.formatting_tag_form = FormattingTagForm(self)
self.shortcut_form = ShortcutListForm(self)
@ -495,10 +492,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.update_recent_files_menu()
self.plugin_form = PluginForm(self)
# Set up signals and slots
self.media_manager_dock.visibilityChanged.connect(self.view_media_manager_item.setChecked)
self.service_manager_dock.visibilityChanged.connect(self.view_service_manager_item.setChecked)
self.theme_manager_dock.visibilityChanged.connect(self.view_theme_manager_item.setChecked)
self.projector_manager_dock.visibilityChanged.connect(self.view_projector_manager_item.setChecked)
self.media_manager_dock.visibilityChanged.connect(self.toggle_media_manager)
self.service_manager_dock.visibilityChanged.connect(self.toggle_service_manager)
self.theme_manager_dock.visibilityChanged.connect(self.toggle_theme_manager)
self.projector_manager_dock.visibilityChanged.connect(self.toggle_projector_manager)
self.import_theme_item.triggered.connect(self.theme_manager_contents.on_import_theme)
self.export_theme_item.triggered.connect(self.theme_manager_contents.on_export_theme)
self.web_site_item.triggered.connect(self.on_help_web_site_clicked)
@ -526,6 +523,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
# Reset the cursor
self.application.set_normal_cursor()
# Starting up web services
self.http_server = HttpServer(self)
self.ws_server = WebSocketServer()
def _wait_for_threads(self):
"""
@ -661,10 +661,14 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.set_view_mode(False, True, False, False, True, True)
self.mode_live_item.setChecked(True)
else:
self.set_view_mode(True, True, True,
self.settings.value('user interface/preview panel'),
self.settings.value('user interface/live panel'),
True)
self.set_view_mode(
self.settings.value('user interface/show library'),
self.settings.value('user interface/show service'),
self.settings.value('user interface/show themes'),
self.settings.value('user interface/preview panel'),
self.settings.value('user interface/live panel'),
self.settings.value('user interface/show projectors')
)
def first_time(self):
"""
@ -1154,15 +1158,21 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
Toggle the visibility of the media manager
"""
self.media_manager_dock.setVisible(not self.media_manager_dock.isVisible())
if self.sender() is self.view_media_manager_item:
self.media_manager_dock.setVisible(not self.media_manager_dock.isVisible())
self.view_media_manager_item.setChecked(self.media_manager_dock.isVisible())
self.settings.setValue('user interface/is preset layout', False)
self.settings.setValue('user interface/show library', self.media_manager_dock.isVisible())
def toggle_projector_manager(self):
"""
Toggle visibility of the projector manager
"""
self.projector_manager_dock.setVisible(not self.projector_manager_dock.isVisible())
if self.sender() is self.view_projector_manager_item:
self.projector_manager_dock.setVisible(not self.projector_manager_dock.isVisible())
self.view_projector_manager_item.setChecked(self.projector_manager_dock.isVisible())
self.settings.setValue('user interface/is preset layout', False)
self.settings.setValue('user interface/show projectors', self.projector_manager_dock.isVisible())
# Check/uncheck checkbox on First time wizard based on visibility of this panel.
if not self.settings.value('projector/show after wizard'):
self.settings.setValue('projector/show after wizard', True)
@ -1173,15 +1183,21 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
"""
Toggle the visibility of the service manager
"""
self.service_manager_dock.setVisible(not self.service_manager_dock.isVisible())
if self.sender() is self.view_service_manager_item:
self.service_manager_dock.setVisible(not self.service_manager_dock.isVisible())
self.view_service_manager_item.setChecked(self.service_manager_dock.isVisible())
self.settings.setValue('user interface/is preset layout', False)
self.settings.setValue('user interface/show service', self.service_manager_dock.isVisible())
def toggle_theme_manager(self):
"""
Toggle the visibility of the theme manager
"""
self.theme_manager_dock.setVisible(not self.theme_manager_dock.isVisible())
if self.sender() is self.view_theme_manager_item:
self.theme_manager_dock.setVisible(not self.theme_manager_dock.isVisible())
self.view_theme_manager_item.setChecked(self.theme_manager_dock.isVisible())
self.settings.setValue('user interface/is preset layout', False)
self.settings.setValue('user interface/show themes', self.theme_manager_dock.isVisible())
def set_preview_panel_visibility(self, visible):
"""

View File

@ -241,7 +241,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.media_info.volume = self.settings.value('media/preview volume')
# background will always loop video.
if service_item.is_capable(ItemCapabilities.HasBackgroundAudio):
controller.media_info.file_info = service_item.background_audio
controller.media_info.file_info = [file_path for (file_path, file_hash) in service_item.background_audio]
controller.media_info.media_type = MediaType.Audio
# is_background indicates we shouldn't override the normal display
controller.media_info.is_background = True

View File

@ -571,8 +571,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
"""
Get a list of files used in the service and files that are missing.
:return: A list of files used in the service that exist, and a list of files that don't.
:rtype: (list[Path], list[str])
:return: A list of tuples with files used in the service that exist and their sha256-based name in the
servicefile, and a list of files that doesn't exists.
:rtype: (list[(Path,str)], list[str])
"""
write_list = []
missing_list = []
@ -589,7 +590,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if item['service_item'].stored_filename:
sha256_file_name = Path(item['service_item'].stored_filename)
else:
sha256_file_name = Path(sha256_file_hash(frame_path)) / frame_path.suffix
sha256_file_name = Path(sha256_file_hash(frame_path) + frame_path.suffix)
bundle = (frame_path, sha256_file_name)
if bundle in write_list or str(frame_path) in missing_list:
continue
@ -626,8 +627,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
if path_from_tuple in write_list:
continue
write_list.append(path_from_tuple)
for audio_path in item['service_item'].background_audio:
service_path = sha256_file_hash(audio_path) + os.path.splitext(audio_path)[1]
for (audio_path, audio_file_hash) in item['service_item'].background_audio:
service_path = audio_file_hash + audio_path.suffix
audio_path_tuple = (audio_path, service_path)
if audio_path_tuple in write_list:
continue

View File

@ -402,7 +402,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
theme_data.background_filename = self.theme_path / new_theme_name / theme_data.background_filename.name
theme_data.theme_name = new_theme_name
theme_data.extend_image_filename(self.theme_path)
self.save_theme(theme_data, background_override=old_background)
self.save_theme(theme_data, background_file=old_background)
self.update_preview_images([new_theme_name])
self.load_themes()
@ -722,12 +722,12 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
return False
return True
def save_theme(self, theme, background_override=None):
def save_theme(self, theme, background_file=None):
"""
Writes the theme to the disk and including the background image and thumbnail if necessary
:param Theme theme: The theme data object.
:param background_override: Background to use rather than background_source. Optionally.
:param background_file: Background to use rather than background_source. Optional.
:rtype: None
"""
name = theme.theme_name
@ -739,15 +739,14 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
theme_path.write_text(theme_pretty)
except OSError:
self.log_exception('Saving theme to file failed')
if theme.background_source and theme.background_filename and theme.background_type != 'stream':
background_file = background_override
if theme.background_filename and theme.background_type != 'stream':
# Use theme source image if override doesn't exist
if not background_file or not background_file.exists():
background_file = theme.background_source
if self.old_background_image_path and theme.background_filename != self.old_background_image_path:
delete_file(self.old_background_image_path)
if not background_file.exists():
self.log_warning('Background does not exist, retaining cached background')
if not background_file or not background_file.exists():
self.log_warning('Background source does not exist, retaining cached background')
elif background_file != theme.background_filename:
try:
shutil.copyfile(background_file, theme.background_filename)

View File

@ -116,7 +116,11 @@ class WordProjectBible(BibleImport):
header_div = soup.find('div', 'textHeader')
chapters_p = header_div.find('p')
if not chapters_p:
chapters_p = soup.p
log.debug('Corrupted header, searching for span instead of a p')
chapters_p = header_div.find('span')
if not chapters_p:
log.debug('Cannot find chapters, using all p instead')
chapters_p = soup.p
log.debug(chapters_p)
for item in chapters_p.contents:
if self.stop_import_flag:

View File

@ -38,7 +38,6 @@ from PyQt5 import QtCore
from openlp.core.common import delete_file, get_uno_command, get_uno_instance, trace_error_handler
from openlp.core.common.platform import is_win
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
TextType
@ -333,7 +332,7 @@ class ImpressDocument(PresentationDocument):
# but OpenLP sets screen numbers based on screen coordinates (geometry)
# so we work out what OpenOffice display to use based on which of the openlp screens is primary
# unless the user has defined in Settings to use the impress Slide Show setting for presentation display
if not Settings().value('presentations/impress use display setting'):
if not self.settings.value('presentations/impress use display setting'):
public_display_screen_number = ScreenList().current.number
screens = list(ScreenList())
if screens[public_display_screen_number].is_primary:

View File

@ -180,23 +180,24 @@ class LibreOfficeServer(object):
if hasattr(self, '_docs'):
while self._docs:
self._docs[0].close_presentation()
docs = self.desktop.getComponents()
count = 0
if docs.hasElements():
list_elements = docs.createEnumeration()
while list_elements.hasMoreElements():
doc = list_elements.nextElement()
if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
count += 1
if count > 0:
log.debug('LibreOffice not terminated as docs are still open')
can_kill = False
else:
try:
self.desktop.terminate()
log.debug('LibreOffice killed')
except Exception:
log.exception('Failed to terminate LibreOffice')
if self.desktop:
docs = self.desktop.getComponents()
count = 0
if docs.hasElements():
list_elements = docs.createEnumeration()
while list_elements.hasMoreElements():
doc = list_elements.nextElement()
if doc.getImplementationName() != 'com.sun.star.comp.framework.BackingComp':
count += 1
if count > 0:
log.debug('LibreOffice not terminated as docs are still open')
can_kill = False
else:
try:
self.desktop.terminate()
log.debug('LibreOffice killed')
except Exception:
log.exception('Failed to terminate LibreOffice')
if getattr(self, '_process') and can_kill:
self._process.kill()

View File

@ -114,7 +114,12 @@ class MacLOController(PresentationController, LogMixin):
Called at system exit to clean up any running presentations.
"""
log.debug('Kill LibreOffice')
self.client.shutdown()
try:
# Some people like to close LibreOffice themselves, let's just catch any errors so that OpenLP fails
# silently
self.client.shutdown()
except Exception:
pass
self.server_process.kill()

View File

@ -300,6 +300,9 @@ class PresentationMediaItem(FolderLibraryItem):
return False
items = [self.list_view.itemFromIndex(item) if isinstance(item, QtCore.QModelIndex) else item
for item in items]
# If this is a folder, show an error message and return
if items and isinstance(items[0].data(0, QtCore.Qt.UserRole), Folder):
return False
if file_path is None:
file_path = Path(items[0].data(0, QtCore.Qt.UserRole).file_path)
file_type = file_path.suffix.lower()[1:]

View File

@ -28,7 +28,6 @@ from openlp.core.common import Singleton, md5_hash, sha256_file_hash
from openlp.core.common.applocation import AppLocation
from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib import create_thumb
@ -147,7 +146,7 @@ class PresentationDocument(object):
# get_temp_folder and PresentationPluginapp_startup is removed
if self.settings.value('presentations/thumbnail_scheme') == 'md5':
folder = md5_hash(bytes(self.file_path))
elif Settings().value('presentations/thumbnail_scheme') == 'sha256file':
elif self.settings.value('presentations/thumbnail_scheme') == 'sha256file':
if self._sha256_file_hash:
folder = self._sha256_file_hash
else:
@ -170,7 +169,7 @@ class PresentationDocument(object):
# get_thumbnail_folder and PresentationPluginapp_startup is removed
if self.settings.value('presentations/thumbnail_scheme') == 'md5':
folder = md5_hash(bytes(self.file_path))
elif Settings().value('presentations/thumbnail_scheme') == 'sha256file':
elif self.settings.value('presentations/thumbnail_scheme') == 'sha256file':
if self._sha256_file_hash:
folder = self._sha256_file_hash
else:

View File

@ -103,6 +103,13 @@ class Book(BaseModel):
"""
Book model
"""
@property
def songs(self):
"""
A property to return the songs associated with this book.
"""
return [sbe.song for sbe in self.entries]
def __repr__(self):
return '<Book id="{myid:d}" name="{name}" publisher="{publisher}" />'.format(myid=self.id,
name=self.name,
@ -237,8 +244,10 @@ def init_schema(url):
**media_files Table**
* id
* _file_path
* file_path
* file_hash
* type
* weight
**song_books Table**
The *song_books* table holds a list of books that a congregation gets
@ -305,6 +314,7 @@ def init_schema(url):
Column('id', types.Integer(), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), default=None),
Column('file_path', PathType, nullable=False),
Column('file_hash', types.Unicode(128), nullable=False),
Column('type', types.Unicode(64), nullable=False, default='audio'),
Column('weight', types.Integer(), default=0)
)
@ -383,7 +393,7 @@ def init_schema(url):
class_mapper(SongBookEntry)
except UnmappedClassError:
mapper(SongBookEntry, songs_songbooks_table, properties={
'songbook': relation(Book)
'songbook': relation(Book, backref='entries')
})
try:
class_mapper(Book)

View File

@ -24,6 +24,7 @@ The :mod:`zionworx` module provides the functionality for importing ZionWorx son
import csv
import logging
from openlp.core.common import get_file_encoding
from openlp.core.common.i18n import translate
from openlp.plugins.songs.lib.importers.songimport import SongImport
@ -72,8 +73,10 @@ class ZionWorxImport(SongImport):
"""
Receive a CSV file (from a ZionWorx database dump) to import.
"""
# Encoding should always be ISO-8859-1
with self.import_source.open('rt', encoding='ISO-8859-1') as songs_file:
# Try to detect encoding, fall back to UTF-8 / ISO-8859-1
encoding = get_file_encoding(self.import_source)
log.info(f'Encoding: {encoding}')
with self.import_source.open('rt', encoding=encoding) as songs_file:
field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords',
'DefaultStyle']
songs_reader = csv.DictReader(songs_file, field_names)

View File

@ -81,10 +81,10 @@ class SongMediaItem(MediaManagerItem):
song.media_files = []
for i, bga in enumerate(item.background_audio):
dest_path =\
AppLocation.get_section_data_path(self.plugin.name) / 'audio' / str(song.id) / os.path.split(bga)[1]
AppLocation.get_section_data_path(self.plugin.name) / 'audio' / str(song.id) / os.path.split(bga[0])[1]
create_paths(dest_path.parent)
copyfile(AppLocation.get_section_data_path('servicemanager') / bga, dest_path)
song.media_files.append(MediaFile.populate(weight=i, file_path=dest_path))
copyfile(AppLocation.get_section_data_path('servicemanager') / bga[0], dest_path)
song.media_files.append(MediaFile.populate(weight=i, file_path=dest_path, file_hash=bga[1]))
self.plugin.manager.save_object(song, True)
def add_middle_header_bar(self):
@ -534,6 +534,7 @@ class SongMediaItem(MediaManagerItem):
copyfile(media_file.file_path, new_media_file_path)
new_media_file = MediaFile()
new_media_file.file_path = new_media_file_path
new_media_file.file_hash = media_file.file_hash
new_media_file.type = media_file.type
new_media_file.weight = media_file.weight
new_song.media_files.append(new_media_file)
@ -629,7 +630,7 @@ class SongMediaItem(MediaManagerItem):
total_length = 0
for m in song.media_files:
total_length += self.media_controller.media_length(m.file_path)
service_item.background_audio = [m.file_path for m in song.media_files]
service_item.background_audio = [(m.file_path, m.file_hash) for m in song.media_files]
service_item.set_media_length(total_length)
service_item.metadata.append('<em>{label}:</em> {media}'.
format(label=translate('SongsPlugin.MediaItem', 'Media'),

View File

@ -29,14 +29,15 @@ from pathlib import Path
from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.sql.expression import false, func, null, text
from openlp.core.common import sha256_file_hash
from openlp.core.common.applocation import AppLocation
from openlp.core.common.db import drop_columns
from openlp.core.common.json import OpenLPJSONEncoder
from openlp.core.common.json import OpenLPJSONEncoder, OpenLPJSONDecoder
from openlp.core.lib.db import PathType, get_upgrade_op
log = logging.getLogger(__name__)
__version__ = 7
__version__ = 8
# TODO: When removing an upgrade path the ftw-data needs updating to the minimum supported version
@ -190,3 +191,28 @@ def upgrade_7(session, metadata):
else:
op.drop_constraint('media_files', 'foreignkey')
op.drop_column('media_files', 'filenames')
def upgrade_8(session, metadata):
"""
Version 8 upgrade - add sha256 hash to media
"""
log.debug('Starting upgrade_8 for adding sha256 hashes')
old_table = Table('media_files', metadata, autoload=True)
if 'file_hash' not in [col.name for col in old_table.c.values()]:
op = get_upgrade_op(session)
op.add_column('media_files', Column('file_hash', types.Unicode(128)))
conn = op.get_bind()
results = conn.execute('SELECT * FROM media_files')
data_path = AppLocation.get_data_path()
for row in results.fetchall():
file_path = json.loads(row.file_path, cls=OpenLPJSONDecoder)
full_file_path = data_path / file_path
if full_file_path.exists():
hash = sha256_file_hash(full_file_path)
else:
log.warning('{audio} does not exists, so no sha256 hash added.'.format(audio=str(file_path)))
# set a fake "hash" to allow for the upgrade to go through. The image will be marked as invalid
hash = 'NONE'
sql = 'UPDATE media_files SET file_hash = :hash WHERE id = :id'
conn.execute(sql, {'hash': hash, 'id': row.id})

View File

@ -21,6 +21,7 @@
"""
Functional tests to test the Http Server Class.
"""
from pathlib import Path
from unittest.mock import patch
from openlp.core.api.http.server import HttpServer
@ -29,14 +30,17 @@ from openlp.core.common.registry import Registry
@patch('openlp.core.api.http.server.HttpWorker')
@patch('openlp.core.api.http.server.run_thread')
def test_server_start(mocked_run_thread, MockHttpWorker, registry):
@patch('openlp.core.api.deploy.AppLocation.get_section_data_path')
def test_server_start(mocked_get_section_data_path, mocked_run_thread, MockHttpWorker, registry):
"""
Test the starting of the Waitress Server with the disable flag set off
"""
# GIVEN: A new httpserver
# GIVEN: A new httpserver and mocked get_section_data_path
mocked_get_section_data_path.return_value = Path('.')
# WHEN: I start the server
Registry().set_flag('no_web_server', False)
HttpServer()
server = HttpServer()
server.bootstrap_post_set_up()
# THEN: the api environment should have been created
assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
@ -45,14 +49,18 @@ def test_server_start(mocked_run_thread, MockHttpWorker, registry):
@patch('openlp.core.api.http.server.HttpWorker')
@patch('openlp.core.api.http.server.run_thread')
def test_server_start_not_required(mocked_run_thread, MockHttpWorker, registry):
@patch('openlp.core.api.deploy.AppLocation.get_section_data_path')
def test_server_start_not_required(mocked_get_section_data_path, mocked_run_thread, MockHttpWorker, registry):
"""
Test the starting of the Waitress Server with the disable flag set off
"""
# GIVEN: A new httpserver
# GIVEN: A new httpserver and mocked get_section_data_path
mocked_get_section_data_path.return_value = Path('.')
# WHEN: I start the server
Registry().set_flag('no_web_server', True)
HttpServer()
server = HttpServer()
server.bootstrap_post_set_up()
# THEN: the api environment should have been created
assert mocked_run_thread.call_count == 0, 'The qthread should not have have been called'

View File

@ -32,6 +32,7 @@ def test_retrieve_live_items(flask_client, settings):
fake_live_controller = MagicMock()
fake_live_controller.service_item = MagicMock()
fake_live_controller.selected_row = 0
fake_live_controller.service_item.unique_identifier = 42
fake_live_controller.service_item.to_dict.return_value = {'slides': [{'selected': False}]}
Registry().register('live_controller', fake_live_controller)
@ -39,7 +40,7 @@ def test_retrieve_live_items(flask_client, settings):
res = flask_client.get('/api/v2/controller/live-items').get_json()
# THEN: The correct item data should be returned
assert res == {'slides': [{'selected': True}]}
assert res == {'slides': [{'selected': True}], 'id': '42'}
def test_controller_set_requires_login(settings, flask_client):

View File

@ -233,17 +233,17 @@ def test_format_slide(settings):
with patch('openlp.core.display.render.ThemePreviewRenderer.__init__') as init_fn:
init_fn.return_value = None
preview_renderer = ThemePreviewRenderer()
lyrics = 'hello {st}test{/st}\nline two\n[---]\nline after optional split'
lyrics = 'hello {st}test{/st}\nline two line after a {st}nice{/st} new line'
preview_renderer._is_initialised = True
preview_renderer.log_debug = MagicMock()
preview_renderer._text_fits_on_slide = MagicMock(side_effect=lambda a: a == '')
preview_renderer._text_fits_on_slide = MagicMock(side_effect=lambda a: len(a) < 80)
preview_renderer.force_page = False
# WHEN: format_slide is run
formatted_slides = preview_renderer.format_slide(lyrics, None)
# THEN: The formatted slides should have all the text and no blank slides
assert formatted_slides == ['hello {st}test{/st}', 'line two', 'line after optional split']
# THEN: The formatted slides should have all the text, no blank slides and formatting tags should still be there
assert formatted_slides == ['hello {st}test{/st}', 'line two line after a {st}nice{/st} new line']
def test_format_slide_no_split(settings):

View File

@ -88,9 +88,11 @@ def test_create_screen_list(mocked_screens, settings):
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is False
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False
assert screen_list.screens[1].is_display is True
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
@ -415,3 +417,183 @@ def test_screen_repr():
# THEN: The string should be correct (screens are 0-based)
assert screen_str == '<Screen 2 (primary)>'
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_screen_removed(mocked_screens, settings):
"""Test that the screen list is correct after a new screen is removed"""
# GIVEN: A screenlist of a mocked application with two screens
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
mocked_application.primaryScreen.return_value = mocked_screen1
screen_list = ScreenList.create(mocked_application)
# WHEN: Screen 2 is removed from the application
mocked_application.screens.return_value = [mocked_screen1]
screen_list.on_screen_removed(mocked_screen2)
# THEN: We have 1 primary screen left in the list
assert len(screen_list.screens) == 1
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is True
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_screen_added(mocked_screens, settings):
"""Test that the screen list is correct after a screen is added"""
# GIVEN: A screenlist of a mocked application with one screen
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1]
mocked_application.primaryScreen.return_value = mocked_screen1
screen_list = ScreenList.create(mocked_application)
# WHEN: Screen 2 is added to the application
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
screen_list.on_screen_added(mocked_screen2)
# THEN: We have 2 screens, one primary, one display
assert len(screen_list.screens) == 2
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is False
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False
assert screen_list.screens[1].is_display is True
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_screen_removed_added(mocked_screens, settings):
"""
Test that the screen list is correct after a screen disappears and reappears again.
This is a common scenario when a secondary screen (possibly a projector) is turned on and off, goes into standby
or has a unstable connection. When the secondary screen returns it should also be marked display as before.
"""
# GIVEN: A screenlist of a mocked application with two screens
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
mocked_application.primaryScreen.return_value = mocked_screen1
# Create the screenlist with both screens present
screen_list = ScreenList.create(mocked_application)
# WHEN: Screen 2 is removed and added again
# Remove screen 2
mocked_application.screens.return_value = [mocked_screen1]
screen_list.on_screen_removed(mocked_screen2)
# Add screen 2
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
screen_list.on_screen_added(mocked_screen2)
# THEN: We have 2 screens, one primary, one display
assert len(screen_list.screens) == 2
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is False
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False
assert screen_list.screens[1].is_display is True
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_third_screen_added(mocked_screens, settings):
"""Test that the screen list is correct after a third screen is added"""
# GIVEN: A screenlist of a mocked application with one screen
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
mocked_application.primaryScreen.return_value = mocked_screen1
screen_list = ScreenList.create(mocked_application)
# WHEN: Screen 2 is added to the application
mocked_screen3 = MagicMock(**{'geometry.return_value': QtCore.QRect(2048, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2, mocked_screen3]
screen_list.on_screen_added(mocked_screen3)
# THEN: We have 3 screens, one primary, one display, one that is neither.
assert len(screen_list.screens) == 3
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is False
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False
assert screen_list.screens[1].is_display is True
assert screen_list.screens[2].number == 2
assert screen_list.screens[2].geometry == QtCore.QRect(2048, 0, 1024, 768)
assert screen_list.screens[2].is_primary is False
assert screen_list.screens[2].is_display is False
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_third_screen_removed(mocked_screens, settings):
"""Test that the screen list is correct after a third screen is removed"""
# GIVEN: A screenlist of a mocked application with one screen
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_screen3 = MagicMock(**{'geometry.return_value': QtCore.QRect(2048, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2, mocked_screen3]
mocked_application.primaryScreen.return_value = mocked_screen1
screen_list = ScreenList.create(mocked_application)
# WHEN: Screen 2 is added to the application
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
screen_list.on_screen_removed(mocked_screen3)
# THEN: We have 3 screens, one primary, one display, one that is neither.
assert len(screen_list.screens) == 2
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is True
assert screen_list.screens[0].is_display is False
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False
assert screen_list.screens[1].is_display is True
@patch('openlp.core.display.screens.QtWidgets.QApplication.screens')
def test_swap_primary_screen(mocked_screens, settings):
"""Test that the screen list is correct after a different screen becomes a primary screen"""
# GIVEN: A screenlist of a mocked application with two screens
mocked_application = MagicMock()
mocked_screen1 = MagicMock(**{'geometry.return_value': QtCore.QRect(0, 0, 1024, 768)})
mocked_screen2 = MagicMock(**{'geometry.return_value': QtCore.QRect(1024, 0, 1024, 768)})
mocked_application.screens.return_value = [mocked_screen1, mocked_screen2]
mocked_application.primaryScreen.return_value = mocked_screen1
# Create the screenlist with the initial state
screen_list = ScreenList.create(mocked_application)
# THEN: Make screen 2 the primary screen
mocked_application.primaryScreen.return_value = mocked_screen2
screen_list.on_primary_screen_changed()
# THEN: The second screen is now primary
assert len(screen_list.screens) == 2
assert screen_list.screens[0].number == 0
assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
assert screen_list.screens[0].is_primary is False
assert screen_list.screens[0].is_display is True
assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is True
assert screen_list.screens[1].is_display is False

View File

@ -412,7 +412,8 @@ def test_service_item_load_optical_media_from_service(state_media):
assert service_item.media_length == 17.694, 'Media length should be 17.694'
def test_service_item_load_song_and_audio_from_service(state_media, settings, service_item_env):
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_service_item_load_song_and_audio_from_service(mock_sha256_file_hash, state_media, settings, service_item_env):
"""
Test the Service Item - adding a song slide from a saved service
"""
@ -420,6 +421,7 @@ def test_service_item_load_song_and_audio_from_service(state_media, settings, se
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
FormattingTags.load_tags()
mock_sha256_file_hash.return_value = 'abcd'
# WHEN: We add a custom from a saved service
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
@ -436,8 +438,8 @@ def test_service_item_load_song_and_audio_from_service(state_media, settings, se
'"Amazing Grace! how sweet the s" has been returned as the title'
assert 'Twas grace that taught my hea' == service_item.get_frame_title(1), \
'"Twas grace that taught my hea" has been returned as the title'
assert Path('/test/amazing_grace.mp3') == service_item.background_audio[0], \
'"/test/amazing_grace.mp3" should be in the background_audio list'
assert (Path('/test/amazing_grace.mp3'), 'abcd') == service_item.background_audio[0], \
'The tuple ("/test/abcd.mp3", "abcd") should be in the background_audio list'
def test_service_item_get_theme_data_global_level(settings):
@ -692,7 +694,8 @@ def test_get_transition_delay_slow(settings):
assert delay == 2
def test_to_dict_text_item(state_media, settings, service_item_env):
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_to_dict_text_item(mocked_sha256_file_hash, state_media, settings, service_item_env):
"""
Test that the to_dict() method returns the correct data for the service item
"""
@ -701,6 +704,7 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
mocked_plugin.name = 'songs'
service_item = ServiceItem(mocked_plugin)
service_item.add_icon = MagicMock()
mocked_sha256_file_hash.return_value = 'abcd'
FormattingTags.load_tags()
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
if is_win():
@ -713,11 +717,11 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
result = service_item.to_dict()
# THEN: The correct dictionary should be returned
expected_fake_path = str(fake_path / 'amazing_grace.mp3')
expected_fake_path = fake_path / 'amazing_grace.mp3'
expected_dict = {
'audit': ['Amazing Grace', ['John Newton'], '', ''],
'backgroundAudio': [expected_fake_path],
'backgroundAudio': [(expected_fake_path, 'abcd')],
'capabilities': [2, 1, 5, 8, 9, 13, 15],
'footer': ['Amazing Grace', 'Written by: John Newton'],
'fromPlugin': False,
@ -726,11 +730,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'notes': '',
'slides': [
{
'chords': '<span class="nochordline">'
'Amazing Grace! how sweet the sound\n'
'chords': 'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n'
'I once was lost, but now am found,\n'
'Was blind, but now I see.</span>',
'Was blind, but now I see.',
'html': 'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n'
'I once was lost, but now am found,\n'
@ -745,11 +748,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': '<span class="nochordline">'
'Twas grace that taught my heart to fear,\n'
'chords': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
'The hour I first believed!</span>',
'The hour I first believed!',
'html': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
@ -764,11 +766,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': '<span class="nochordline">'
'Through many dangers, toils and snares\n'
'chords': 'Through many dangers, toils and snares\n'
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.</span>',
'And grace will lead me home.',
'html': 'Through many dangers, toils and snares\n'
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
@ -783,11 +784,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': '<span class="nochordline">'
'The Lord has promised good to me,\n'
'chords': 'The Lord has promised good to me,\n'
'His word my hope secures;\n'
'He will my shield and portion be\n'
'As long as life endures.</span>',
'As long as life endures.',
'html': 'The Lord has promised good to me,\n'
'His word my hope secures;\n'
'He will my shield and portion be\n'
@ -802,11 +802,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': '<span class="nochordline">'
'Yes, when this heart and flesh shall fail,\n'
'chords': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
'A life of joy and peace.</span>',
'A life of joy and peace.',
'html': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
@ -821,11 +820,10 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': '<span class="nochordline">'
'When weve been there a thousand years,\n'
'chords': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'
'Than when we first begun.</span>',
'Than when we first begun.',
'html': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'

View File

@ -86,7 +86,7 @@ def projector_manager_mtdb(settings):
def fake_pjlink():
faker = FakePJLink()
yield faker
del(faker)
del faker
@pytest.fixture()

View File

@ -72,6 +72,7 @@ def main_window(state, settings, mocked_qapp):
mocked_qapp.primaryScreen.return_value = mocked_screen
ScreenList.create(mocked_qapp)
mainwindow = MainWindow()
mainwindow.activateWindow = MagicMock()
yield mainwindow
del mainwindow
renderer_patcher.stop()
@ -348,10 +349,10 @@ def test_mainwindow_configuration(main_window):
# WHEN: you check the started functions
# THEN: the following registry functions should have been registered
expected_service_list = ['settings', 'settings_thread', 'application', 'main_window', 'http_server',
'authentication_token', 'settings_form', 'service_manager', 'theme_manager',
'projector_manager']
expected_functions_list = ['bootstrap_initialise', 'bootstrap_post_set_up', 'bootstrap_completion',
expected_service_list = ['settings', 'settings_thread', 'application', 'main_window', 'settings_form',
'service_manager', 'theme_manager', 'projector_manager', 'http_server',
'authentication_token', 'web_socket_server']
expected_functions_list = ['bootstrap_post_set_up', 'bootstrap_initialise', 'bootstrap_completion',
'config_screen_changed', 'theme_change_global']
assert list(Registry().service_list.keys()) == expected_service_list, \
'The service list should have been {}'.format(Registry().service_list.keys())
@ -579,8 +580,63 @@ def test_projector_manager_dock_unlocked(main_window_reduced):
projector_dock.setFeatures.assert_called_with(7)
@patch('openlp.core.ui.mainwindow.MainWindow.open_cmd_line_files')
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_load_settings_view_mode_default_mode(mocked_view_mode, main_window, settings):
def test_show_cmd_line_args(mocked_view_mode, mocked_open_cmd_line_files, main_window, settings):
"""Test that command line arguments are loaded on show"""
# GIVEN: A newly opened OpenLP instance with some command line arguments
main_window.application.args = ['-disable-web-security', 'new_file_name.osz']
# WHEN: MainWindow.show() is called
main_window.show()
# THEN: The command line arguments are searched for a file name
mocked_open_cmd_line_files.assert_called_once_with(main_window.application.args)
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_show_load_last_file(mocked_view_mode, main_window, settings):
"""Test that the last file opened is loaded on show"""
# GIVEN: A newly opened OpenLP instance with some command line arguments
main_window.service_manager_contents.load_last_file = MagicMock()
main_window.settings.setValue('core/auto open', True)
# WHEN: MainWindow.show() is called
main_window.show()
# THEN: The command line arguments are searched for a file name
main_window.service_manager_contents.load_last_file.assert_called_once_with()
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_load_settings_custom_layout(mocked_view_mode, main_window, settings):
"""Test that the view mode is called with the correct parameters for default mode"""
# GIVEN a newly opened OpenLP instance, mocked screens and settings for a valid window position
# mock out some other calls in load_settings()
main_window.control_splitter = MagicMock()
main_window._live_controller = MagicMock()
main_window._preview_controller = MagicMock()
main_window.activateWindow = MagicMock()
main_window.settings.setValue('core/view mode', 'default')
main_window.settings.setValue('user interface/is preset layout', False)
main_window.settings.setValue('user interface/show library', True)
main_window.settings.setValue('user interface/show service', False)
main_window.settings.setValue('user interface/show themes', True)
main_window.settings.setValue('user interface/show projectors', False)
main_window.settings.setValue('user interface/live panel', True)
main_window.settings.setValue('user interface/preview panel', False)
# WHEN: we call to show method
main_window.show()
# THEN:
# The default mode should have been called.
mocked_view_mode.assert_called_with(True, True, True, False, True, False)
@patch('openlp.core.ui.mainwindow.QtWidgets.QWidget.show')
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_load_settings_view_mode_default_mode(mocked_view_mode, mocked_show, main_window, settings):
"""
Test that the view mode is called with the correct parameters for default mode
"""
@ -589,10 +645,11 @@ def test_load_settings_view_mode_default_mode(mocked_view_mode, main_window, set
main_window.control_splitter = MagicMock()
main_window._live_controller = MagicMock()
main_window._preview_controller = MagicMock()
main_window.activateWindow = MagicMock()
main_window.settings.setValue('core/view mode', 'default')
main_window.settings.setValue('user interface/is preset layout', True)
# WHENL we call to show method
# WHEN: we call to show method
main_window.show()
# THEN:
@ -600,10 +657,14 @@ def test_load_settings_view_mode_default_mode(mocked_view_mode, main_window, set
mocked_view_mode.assert_called_with(True, True, True, True, True, True)
@patch('openlp.core.ui.mainwindow.QtWidgets.QWidget.show')
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_load_settings_view_mode_setup_mode(mocked_view_mode, main_window, settings):
def test_load_settings_view_mode_setup_mode(mocked_view_mode, mocked_show, main_window, settings):
"""
Test that the view mode is called with the correct parameters for setup mode
Note: for some reason, only when the test is running, the QWidget.show() method resets the layout setting,
so this is mocked out to prevent it from interferring in the loading of the view layout
"""
# GIVEN a newly opened OpenLP instance, mocked screens and settings for a valid window position
# mock out some other calls in load_settings()
@ -613,7 +674,7 @@ def test_load_settings_view_mode_setup_mode(mocked_view_mode, main_window, setti
main_window.settings.setValue('core/view mode', 'setup')
main_window.settings.setValue('user interface/is preset layout', True)
# WHENL we call to show method
# WHEN: we call to show method
main_window.show()
# THEN:
@ -621,10 +682,14 @@ def test_load_settings_view_mode_setup_mode(mocked_view_mode, main_window, setti
mocked_view_mode.assert_called_with(True, True, False, True, False, True)
@patch('openlp.core.ui.mainwindow.QtWidgets.QWidget.show')
@patch('openlp.core.ui.mainwindow.MainWindow.set_view_mode')
def test_load_settings_view_mode_live_mode(mocked_view_mode, main_window, settings):
def test_load_settings_view_mode_live_mode(mocked_view_mode, mocked_show, main_window, settings):
"""
Test that the view mode is called with the correct parameters for live mode
Note: for some reason, only when the test is running, the QWidget.show() method resets the layout setting,
so this is mocked out to prevent it from interferring in the loading of the view layout
"""
# GIVEN a newly opened OpenLP instance, mocked screens and settings for a valid window position
# mock out some other calls in load_settings()
@ -634,7 +699,7 @@ def test_load_settings_view_mode_live_mode(mocked_view_mode, main_window, settin
main_window.settings.setValue('core/view mode', 'live')
main_window.settings.setValue('user interface/is preset layout', True)
# WHENL we call to show method
# WHEN: we call to show method
main_window.show()
# THEN:
@ -652,11 +717,12 @@ def test_load_settings_view_mode_preview(mocked_view_mode, main_window, settings
main_window.control_splitter = MagicMock()
main_window._live_controller = MagicMock()
main_window._preview_controller = MagicMock()
main_window.activateWindow = MagicMock()
main_window.settings.setValue('core/view mode', 'default')
main_window.settings.setValue('user interface/is preset layout', False)
main_window.settings.setValue('user interface/preview panel', False)
# WHENL we call to show method
# WHEN: we call to show method
main_window.show()
# THEN:
@ -674,11 +740,12 @@ def test_load_settings_view_mode_live(mocked_view_mode, main_window, settings):
main_window.control_splitter = MagicMock()
main_window._live_controller = MagicMock()
main_window._preview_controller = MagicMock()
main_window.activateWindow = MagicMock()
main_window.settings.setValue('core/view mode', 'default')
main_window.settings.setValue('user interface/is preset layout', False)
main_window.settings.setValue('user interface/live panel', False)
# WHENL we call to show method
# WHEN: we call to show method
main_window.show()
# THEN:

View File

@ -1267,6 +1267,83 @@ def test_load_service_modified_saved_with_file_path(registry):
service_manager.load_file.assert_called_once_with(Path.home() / 'service.osz')
@patch('openlp.core.ui.servicemanager.FileDialog.getOpenFileName')
def test_load_service_no_file_path_passed_or_selected(mocked_get_open_file_name, mock_settings):
"""Test that the load_service() method exits early when no file is passed and no file is selected in the dialog"""
# GIVEN: A modified ServiceManager
service_manager = ServiceManager(None)
mocked_get_open_file_name.return_value = (None, None)
# WHEN: A service is loaded
result = service_manager.load_service()
# THEN: The result should be False because of an early exit
assert result is False, 'The method did not exit early'
@patch('openlp.core.ui.servicemanager.FileDialog.getOpenFileName')
def test_load_service_no_file_path_passed_file_selected(mocked_get_open_file_name, mock_settings):
"""Test that the load_service() method loads a file chosen in the dialog when no file is passed"""
# GIVEN: A modified ServiceManager
service_manager = ServiceManager(None)
mocked_get_open_file_name.return_value = (Path.home() / 'service.osz', None)
service_manager.load_file = MagicMock()
# WHEN: A service is loaded
service_manager.load_service()
# THEN: The service should be loaded
service_manager.load_file.assert_called_once_with(Path.home() / 'service.osz')
def test_on_recent_service_clicked_modified_cancel_save(registry):
"""Test that the on_recent_service_clicked() method exits early when the service is modified,
but the save is canceled"""
# GIVEN: A modified ServiceManager
service_manager = ServiceManager(None)
service_manager.is_modified = MagicMock(return_value=True)
service_manager.save_modified_service = MagicMock(return_value=QtWidgets.QMessageBox.Cancel)
# WHEN: on_recent_service_clicked is called
result = service_manager.on_recent_service_clicked(True)
# THEN: The result should be False because of an early exit
assert result is False, 'The method did not exit early'
def test_on_recent_service_clicked_modified_saved_with_file_path(registry):
"""Test that the on_recent_service_clicked() method saves the file and loads the file"""
# GIVEN: A modified ServiceManager
mocked_settings = MagicMock()
registry.register('settings', mocked_settings)
service_manager = ServiceManager(None)
service_manager.is_modified = MagicMock(return_value=True)
service_manager.save_modified_service = MagicMock(return_value=QtWidgets.QMessageBox.Save)
service_manager.decide_save_method = MagicMock()
service_manager.load_file = MagicMock()
service_manager.sender = MagicMock(return_value=MagicMock())
# WHEN: on_recent_service_clicked is called
service_manager.on_recent_service_clicked(True)
# THEN: The recent service should be loaded
service_manager.decide_save_method.assert_called_once_with()
service_manager.load_file.assert_called_once()
def test_on_recent_service_clicked_unmodified(registry):
# GIVEN: A modified ServiceManager
service_manager = ServiceManager(None)
service_manager.load_file = MagicMock()
service_manager.sender = MagicMock(return_value=MagicMock())
# WHEN: on_recent_service_clicked is called
service_manager.on_recent_service_clicked(True)
# THEN: The recent service should be loaded
service_manager.load_file.assert_called_once()
@patch('openlp.core.ui.servicemanager.Path', autospec=True)
def test_service_manager_load_file_str(MockPath, registry):
"""Test the service manager's load_file method when it is given a str"""

View File

@ -32,6 +32,7 @@ from PyQt5 import QtWidgets
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib.theme import Theme
from openlp.core.ui.thememanager import ThemeManager
from tests.utils.constants import RESOURCE_PATH
@ -265,7 +266,7 @@ def test_save_theme_missing_new(mocked_paths, mocked_delete, mocked_log_warning,
theme_manager.save_theme(mocked_theme)
# THEN: A warning should have happened due to attempting to copy a missing file
mocked_log_warning.assert_called_once_with('Background does not exist, retaining cached background')
mocked_log_warning.assert_called_once_with('Background source does not exist, retaining cached background')
@patch('openlp.core.ui.thememanager.shutil')
@ -292,7 +293,7 @@ def test_save_theme_background_override(mocked_paths, mocked_delete, mocked_shut
override_background = MagicMock()
# WHEN: Calling save_theme with a background override
theme_manager.save_theme(mocked_theme, background_override=override_background)
theme_manager.save_theme(mocked_theme, background_file=override_background)
# THEN: The override_background should have been copied rather than the background_source
mocked_shutil.copyfile.assert_called_once_with(override_background, mocked_theme.background_filename)
@ -506,3 +507,27 @@ def test_bootstrap_post(mocked_rename_form, mocked_theme_form, theme_manager):
assert theme_manager.file_rename_form is not None
theme_manager.upgrade_themes.assert_called_once()
theme_manager.load_themes.assert_called_once()
def test_clone_theme_data(theme_manager):
"""Test that cloning the theme data works correctly"""
# GIVEN: A theme manager, a theme (without a background source) and a new theme name
existing_theme = Theme()
existing_theme.theme_name = 'Existing Theme'
existing_theme.background_type = 'image'
existing_theme.background_filename = Path('Existing Theme', 'background.jpg')
theme_manager.theme_path = Path('openlp', 'themes')
theme_manager.save_theme = MagicMock()
theme_manager.update_preview_images = MagicMock()
theme_manager.load_themes = MagicMock()
# WHEN: The theme is cloned
theme_manager.clone_theme_data(existing_theme, 'New Theme')
# THEN: The theme data should have been updated
assert existing_theme.theme_name == 'New Theme'
theme_manager.save_theme.assert_called_once_with(existing_theme, background_file=Path('Existing Theme',
'background.jpg'))
theme_manager.update_preview_images.assert_called_once_with(['New Theme'])
theme_manager.load_themes.assert_called_once_with()

View File

@ -31,6 +31,7 @@ from tests.utils.constants import RESOURCE_PATH
TEST_PATH = RESOURCE_PATH / 'bibles'
INDEX_PAGE = (TEST_PATH / 'wordproject_index.htm').read_bytes().decode()
CHAPTER_PAGE = (TEST_PATH / 'wordproject_chapter.htm').read_bytes().decode()
CORRUPTED_CHAPTER_PAGE = (TEST_PATH / 'wordproject_chapter_corrupted.htm').read_bytes().decode()
@patch.object(Path, 'read_text')
@ -91,6 +92,35 @@ def test_process_chapters(mocked_read_text, settings):
assert mocked_process_verses.call_args_list == expected_process_verses_calls
@patch.object(Path, 'read_text')
def test_process_chapters_corrupted_header(mocked_read_text, settings):
"""
Test the process_chapters() method when there's a "corrupted" header with a span instead of a p
"""
# GIVEN: A WordProject importer and a bunch of mocked things
importer = WordProjectBible(MagicMock(), path='.', name='.', file_path=Path('kj.zip'))
importer.base_path = Path()
importer.stop_import_flag = False
importer.language_id = 'en'
mocked_read_text.return_value = CORRUPTED_CHAPTER_PAGE
mocked_db_book = MagicMock()
mocked_db_book.name = 'Genesis'
book_id = 1
book_link = '01/1.htm'
# WHEN: process_chapters() is called
with patch.object(importer, 'set_current_chapter') as mocked_set_current_chapter, \
patch.object(importer, 'process_verses') as mocked_process_verses:
importer.process_chapters(mocked_db_book, book_id, book_link)
# THEN: The right methods should have been called
expected_set_current_chapter_calls = [call('Genesis', ch) for ch in range(1, 51)]
expected_process_verses_calls = [call(mocked_db_book, 1, ch) for ch in range(1, 51)]
mocked_read_text.assert_called_once_with(encoding='utf-8', errors='ignore')
assert mocked_set_current_chapter.call_args_list == expected_set_current_chapter_calls
assert mocked_process_verses.call_args_list == expected_process_verses_calls
@patch.object(Path, 'read_text')
def test_process_verses(mocked_read_text, settings):
"""

View File

@ -159,6 +159,23 @@ class TestMacLOController(TestCase, TestMixin):
controller._client.shutdown.assert_called_once_with()
controller.server_process.kill.assert_called_once_with()
@patch('openlp.plugins.presentations.lib.maclocontroller.MacLOController._start_server')
def test_kill_client_already_closed(self, mocked_start_server):
"""
Test the kill() method when the client is already closed
"""
# GIVEN: A controller and a client
controller = MacLOController(plugin=self.mock_plugin)
controller._client = MagicMock(**{'shutdown.side_effect': Exception})
controller.server_process = MagicMock()
# WHEN: start_process() is called
controller.kill()
# THEN: The client's start_process() should have been called
controller._client.shutdown.assert_called_once_with()
controller.server_process.kill.assert_called_once_with()
class TestMacLODocument(TestCase):
"""

View File

@ -24,8 +24,11 @@ This module contains tests for the lib submodule of the Presentations plugin.
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, call, patch
from PyQt5 import QtCore, QtWidgets
from openlp.core.lib import ServiceItemContext
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.plugins.presentations.lib.db import Folder, Item
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
@ -128,7 +131,60 @@ def test_clean_up_thumbnails_missing_file(media_item):
mocked_doc.assert_has_calls([call.get_thumbnail_path(1, True), call.presentation_deleted()], True)
def test_pdf_generate_slide_data(media_item):
def test_generate_slide_data_from_folder(media_item):
"""
Test that the generate slide data function exits early when the item is a Folder instead of an Item
"""
# GIVEN: A Folder instance
media_item.list_view = MagicMock()
mocked_service_item = MagicMock()
folder = Folder(id=1, name='Mock folder')
list_item = QtWidgets.QTreeWidgetItem(None)
list_item.setData(0, QtCore.Qt.UserRole, folder)
# WHEN: generate_slide_data is called
result = media_item.generate_slide_data(mocked_service_item, item=list_item)
# THEN: The result should be false
assert result is False
def test_generate_slide_data_from_list_view(media_item):
"""
Test that the generate slide data function exits early when there are more than 1 items selected
"""
# GIVEN: A Folder instance
mocked_service_item = MagicMock()
list_item = QtWidgets.QTreeWidgetItem(None)
media_item.list_view = MagicMock(selectedItems=MagicMock(return_value=[list_item, list_item]))
# WHEN: generate_slide_data is called
result = media_item.generate_slide_data(mocked_service_item)
# THEN: The result should be false
assert result is False
def test_generate_slide_data_with_file_path_from_item(media_item):
"""
Test that the generate slide data function exits early when there is no display type combobox text
"""
# GIVEN: A Folder instance
media_item.list_view = MagicMock()
media_item.display_type_combo_box = MagicMock(currentText=MagicMock(return_value=''))
mocked_service_item = MagicMock()
item = Item(id=1, file_path='path/to/presentation.odp')
list_item = QtWidgets.QTreeWidgetItem(None)
list_item.setData(0, QtCore.Qt.UserRole, item)
# WHEN: generate_slide_data is called
result = media_item.generate_slide_data(mocked_service_item, item=list_item)
# THEN: The result should be false
assert result is False
def test_generate_slide_data_from_pdf(media_item):
"""
Test that the generate slide data function makes the correct ajustments to a pdf service item.
"""

View File

@ -24,7 +24,7 @@ Package to test the openlp.plugins.songs.forms.songmaintenanceform package.
import pytest
import os
from unittest.mock import MagicMock, call, patch, ANY
from unittest.mock import MagicMock, call, create_autospec, patch, ANY
from PyQt5 import QtCore, QtWidgets
@ -208,10 +208,43 @@ def test_delete_item(mocked_critical_error_message_box, form_env):
mocked_get_current_item_id.assert_called_once_with(mocked_list_widget)
mocked_manager.get_object.assert_called_once_with(mocked_item_class, 1)
mocked_critical_error_message_box.assert_called_once_with(dialog_title, delete_text, form, True)
mocked_manager.delete_object(mocked_item_class, 1)
mocked_manager.delete_object.assert_called_once_with(mocked_item_class, 1)
mocked_reset_func.assert_called_once_with()
@patch('openlp.plugins.songs.forms.songmaintenanceform.critical_error_message_box')
def test_delete_book_assigned(mocked_critical_error_message_box, form_env):
"""
Test the _delete_item() method
"""
# GIVEN: Some mocked items
form = form_env[0]
mocked_manager = form_env[1]
mocked_item = create_autospec(Book, spec_set=True)
mocked_item.id = 1
mocked_manager.get_object.return_value = mocked_item
mocked_critical_error_message_box.return_value = QtWidgets.QMessageBox.Yes
mocked_item_class = MagicMock()
mocked_list_widget = MagicMock()
mocked_reset_func = MagicMock()
dialog_title = 'Delete Book'
delete_text = 'Are you sure you want to delete the selected book?'
error_text = 'This book cannot be deleted, it is currenty assigned to at least one song.'
# WHEN: _delete_item() is called
with patch.object(form, '_get_current_item_id') as mocked_get_current_item_id:
mocked_get_current_item_id.return_value = 1
form._delete_item(mocked_item_class, mocked_list_widget, mocked_reset_func, dialog_title, delete_text,
error_text)
# THEN: The right things should have been called
mocked_get_current_item_id.assert_called_once_with(mocked_list_widget)
mocked_manager.get_object.assert_called_once_with(mocked_item_class, 1)
mocked_critical_error_message_box.assert_called_once_with(dialog_title, error_text)
mocked_manager.delete_object.assert_not_called()
mocked_reset_func.assert_not_called()
@patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem')
@patch('openlp.plugins.songs.forms.songmaintenanceform.Author')
def test_reset_authors(MockedAuthor, MockedQListWidgetItem, form_env):

View File

@ -23,6 +23,8 @@ This module contains tests for the ZionWorx song importer.
"""
from unittest.mock import MagicMock, patch
import pytest
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.importers.zionworx import ZionWorxImport
from tests.helpers.songfileimport import SongImportTestHelper
@ -47,10 +49,11 @@ def test_create_importer(registry):
assert isinstance(importer, SongImport)
def test_zion_wrox(mock_settings):
@pytest.mark.parametrize('filebase', ['zionworx', 'amazing-grace-arabic'])
def test_zion_wrox(filebase, mock_settings):
"""Test that the ZionWorx importer correctly imports songs"""
test_file_import = SongImportTestHelper('ZionWorxImport', 'zionworx')
test_file_import.setUp()
test_file_import.file_import(TEST_PATH / 'zionworx.csv',
test_file_import.load_external_result_data(TEST_PATH / 'zionworx.json'))
test_file_import.file_import(TEST_PATH / f'{filebase}.csv',
test_file_import.load_external_result_data(TEST_PATH / f'{filebase}.json'))
test_file_import.tearDown()

View File

@ -0,0 +1,248 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Creation of the world, Genesis Chapter 1</title>
<meta name="description" content="Creation of the world, Genesis Chapter 1" />
<meta name="keywords" content="Holy Bible, Old Testament, scriptures, Creation, faith, heaven, hell, God, Jesus" />
<!-- Mobile viewport optimisation -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="../_assets/css/css.css" />
<link rel="stylesheet" type="text/css" href="../_assets/css/style.css" />
<link rel="stylesheet" type="text/css" href="../_assets/css/page-player.css" />
<!--[if lte IE 7]>
<link href="../_assets/css/iehacks.css" rel="stylesheet" type="text/css" />
<![endif]-->
<!-- google analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-39700598-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<!-- google analytics -->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-39700598-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<a name="mytop"></a>
<body>
<header class="ym-noprint">
<div class="ym-wrapper">
<div class="ym-wbox">
<h1><strong>Word</strong><em>Project</em></h1>
</div>
</div>
</header>
<!--lang nav-->
<!--nav id="nav">
<div class="ym-wrapper">
<div class="ym-hlist">
<ul>
<li><a title="Home" href="../../../index.htm" target="_top">Home</a></li>
<li class="active"><a title="Bibles" href="../../../bibles/index.htm" target="_self">Bibles</a></li>
<li><a title="Audio Bible" href="../../../bibles/audio/01_english/b01.htm" target="_top">Audio</a></li>
<li><a title="Selected Bible Verses" href="../../../bibles/verses/english/index.htm" target="_top">Verses</a></li>
<li><a title="Parallel Bibles" href="../../../bibles/parallel/index.htm" target="_top">Multi</a></li>
<li><a title="Resourcces" href="../../../bibles/resources/index.htm" target="_top">Resources</a></li>
<li><a title="Search" href="../../../bibles/search/index.htm" target="_top">Search</a></li>
<li><a title="Download this Bible [language]" href="../../../download/bibles/index.htm" target="_top">Download</a></li>
</ul>
</div>
</div>
</nav-->
<div class="ym-wrapper ym-noprint">
<div class="ym-wbox">
<!--share buttons-->
<div style="margin: 10px 1px 5px 20px;" align="right">
<!-- Facebook -->
<a title="Click to share on Facebook" href="http://www.facebook.com/sharer.php?u=http://wordproject.org/bibles/kj/01/1.htm" target="_blank"><img src="../_assets/img/facebook_2.png" alt="facebook" /></a>
<!-- Twitter -->
<a title="Click to share on Twitter" href="http://twitter.com/share?url=http://wordproject.org/bibles/kj/01/1.htm&text=Read this page &hashtags=wordproject" target="_blank"><img src="../_assets/img/twitter_2.png" alt="twitter" /></a>
<!-- Google+ -->
<a title="Click to share on Google plus" href="https://plus.google.com/share?url=http://wordproject.org/bibles/kj/01/1.htm" target="_blank"><img src="../_assets/img/google+_2.png" alt="google" /></a>
<!-- LinkedIn -->
<a title="Click to share on Linkedin" href="http://www.linkedin.com/shareArticle?mini=true&url=http://www.wordproject.org" target="_blank"><img src="../_assets/img/linkin_2.png" alt="linkin" /></a></p>
</div>
<!--/share buttons-->
<div class=" ym-grid">
<div class="ym-g62 ym-gl breadCrumbs"> <!--a title="Home" href="http://www.wordproject.org/index.htm" target="_top">Home</a> / <a title="Bibles" href="../../index.htm" target="_self">Bibles</a--> / <a href="../index.htm" target="_self">KJV</a></div>
<div class="ym-g38 ym-gr alignRight ym-noprint"><a class="decreaseFont ym-button">-</a><a class="resetFont ym-button">Reset</a><a class="increaseFont ym-button">+</a>
</div>
</div>
</div>
</div>
<div id="main" class="ym-clearfix" role="main">
<div class="ym-wrapper">
<div class="ym-wbox">
<div class="textOptions">
<div class="textHeader">
<h2>Genesis</h2>
<a name="0"></a>
<span class="ym-noprint"> Chapter:
<span class="c1">1</span>
<a href="2.htm#0">2</a>
<a href="3.htm#0">3</a>
<a href="4.htm#0">4</a>
<a href="5.htm#0">5</a>
<a href="6.htm#0">6</a>
<a href="7.htm#0">7</a>
<a href="8.htm#0">8</a>
<a href="9.htm#0">9</a>
<a href="10.htm#0">10</a>
<a href="11.htm#0">11</a>
<a href="12.htm#0">12</a>
<a href="13.htm#0">13</a>
<a href="14.htm#0">14</a>
<a href="15.htm#0">15</a>
<a href="16.htm#0">16</a>
<a href="17.htm#0">17</a>
<a href="18.htm#0">18</a>
<a href="19.htm#0">19</a>
<a href="20.htm#0">20</a>
<a href="21.htm#0">21</a>
<a href="22.htm#0">22</a>
<a href="23.htm#0">23</a>
<a href="24.htm#0">24</a>
<a href="25.htm#0">25</a>
<a href="26.htm#0">26</a>
<a href="27.htm#0">27</a>
<a href="28.htm#0">28</a>
<a href="29.htm#0">29</a>
<a href="30.htm#0">30</a>
<a href="31.htm#0">31</a>
<a href="32.htm#0">32</a>
<a href="33.htm#0">33</a>
<a href="34.htm#0">34</a>
<a href="35.htm#0">35</a>
<a href="36.htm#0">36</a>
<a href="37.htm#0">37</a>
<a href="38.htm#0">38</a>
<a href="39.htm#0">39</a>
<a href="40.htm#0">40</a>
<a href="41.htm#0">41</a>
<a href="42.htm#0">42</a>
<a href="43.htm#0">43</a>
<a href="44.htm#0">44</a>
<a href="45.htm#0">45</a>
<a href="46.htm#0">46</a>
<a href="47.htm#0">47</a>
<a href="48.htm#0">48</a>
<a href="49.htm#0">49</a>
<a href="50.htm#0">50</a>
<!--end of chapters-->
</p>
</div>
<div class="textAudio ym-noprint"><ul class="playlist">
<li class="noMargin">
<!--start audio link--><a href="http://audio2.wordproject.com/bibles/app/audio/1/1/1.mp3">Genesis - Chapter 1 </a></li><!--/audioRef-->
</ul>
</div>
<!--end audio-->
<hr />
<div class="textBody" id="textBody">
<h3>Chapter 1</h3>
<!--... the Word of God:--></a>
<p><span class="verse" id="1">1</span> In the beginning God created the heaven and the earth.
<br /><span class="verse" id="2">2</span> And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters.
<br /><span class="verse" id="3">3</span> And God said, Let there be light: and there was light.
<br /><span class="verse" id="4">4</span> And God saw the light, that it was good: and God divided the light from the darkness.
<br /><span class="verse" id="5">5</span> And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day.
<br /><span class="verse" id="6">6</span> And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters.
<br /><span class="verse" id="7">7</span> And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so.
<br /><span class="verse" id="8">8</span> And God called the firmament Heaven. And the evening and the morning were the second day.
<br /><span class="verse" id="9">9</span> And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so.
<br /><span class="verse" id="10">10</span> And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good.
<br /><span class="verse" id="11">11</span> And God said, Let the earth bring forth grass, the herb yielding seed, and the fruit tree yielding fruit after his kind, whose seed is in itself, upon the earth: and it was so.
<br /><span class="verse" id="12">12</span> And the earth brought forth grass, and herb yielding seed after his kind, and the tree yielding fruit, whose seed was in itself, after his kind: and God saw that it was good.
<br /><span class="verse" id="13">13</span> And the evening and the morning were the third day.
<br /><span class="verse" id="14">14</span> And God said, Let there be lights in the firmament of the heaven to divide the day from the night; and let them be for signs, and for seasons, and for days, and years:
<br /><span class="verse" id="15">15</span> And let them be for lights in the firmament of the heaven to give light upon the earth: and it was so.
<br /><span class="verse" id="16">16</span> And God made two great lights; the greater light to rule the day, and the lesser light to rule the night: he made the stars also.
<br /><span class="verse" id="17">17</span> And God set them in the firmament of the heaven to give light upon the earth,
<br /><span class="verse" id="18">18</span> And to rule over the day and over the night, and to divide the light from the darkness: and God saw that it was good.
<br /><span class="verse" id="19">19</span> And the evening and the morning were the fourth day.
<br /><span class="verse" id="20">20</span> And God said, Let the waters bring forth abundantly the moving creature that hath life, and fowl that may fly above the earth in the open firmament of heaven.
<br /><span class="verse" id="21">21</span> And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good.
<br /><span class="verse" id="22">22</span> And God blessed them, saying, Be fruitful, and multiply, and fill the waters in the seas, and let fowl multiply in the earth.
<br /><span class="verse" id="23">23</span> And the evening and the morning were the fifth day.
<br /><span class="verse" id="24">24</span> And God said, Let the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the earth after his kind: and it was so.
<br /><span class="verse" id="25">25</span> And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good.
<br /><span class="verse" id="26">26</span> And God said, Let us make man in our image, after our likeness: and let them have dominion over the fish of the sea, and over the fowl of the air, and over the cattle, and over all the earth, and over every creeping thing that creepeth upon the earth.
<br /><span class="verse" id="27">27</span> So God created man in his own image, in the image of God created he him; male and female created he them.
<br /><span class="verse" id="28">28</span> And God blessed them, and God said unto them, Be fruitful, and multiply, and replenish the earth, and subdue it: and have dominion over the fish of the sea, and over the fowl of the air, and over every living thing that moveth upon the earth.
<br /><span class="verse" id="29">29</span> And God said, Behold, I have given you every herb bearing seed, which is upon the face of all the earth, and every tree, in the which is the fruit of a tree yielding seed; to you it shall be for meat.
<br /><span class="verse" id="30">30</span> And to every beast of the earth, and to every fowl of the air, and to every thing that creepeth upon the earth, wherein there is life, I have given every green herb for meat: and it was so.
<br /><span class="verse" id="31">31</span> And God saw every thing that he had made, and, behold, it was very good. And the evening and the morning were the sixth day.
</p>
<!--... sharper than any twoedged sword... -->
</div>
</div><!-- ym-wbox end -->
</div><!-- ym-wrapper end -->
</div><!-- ym-wrapper end -->
</div><!-- ym-wrapper end -->
<!--..sharper than any twoedged sword...-->
<div class="ym-wrapper">
<div class="ym-wbox">
<div class="alignRight ym-noprint">
<p><a title="Print this page" href="javascript:window.print()" class="ym-button">&nbsp;<img src="../_assets/img/printer.gif" alt="printer" width="25" height="25" align="absbottom" />&nbsp;</a>
<a class="ym-button" title="Page TOP" href="#mytop">&nbsp;<img src="../_assets/img/arrow_up.png" alt="arrowup" width="25" height="25" align="absbottom" />&nbsp;</a>
<!--next chapter start-->
<a class="ym-button" title="Next chapter" href="2.htm#0">&nbsp;<img src="../_assets/img/arrow_right.png" alt="arrowright" align="absbottom" />&nbsp;</a></p>
<!--next chapter end-->
</div>
</div>
</div>
<footer>
<div class="ym-wrapper">
<div class="ym-wbox">
<p class="alignCenter">Wordproject® is a registered name of the <a href="http://www.wordproject.org">International Biblical Association</a>, a non-profit organization registered in Macau, China. </p>
<p class="alignCenter"><a href="http://www.wordproject.org/contact/new/index.htm" target="_top">Contact</a> | <a href="http://www.wordproject.org/contact/new/disclaim.htm" target="_top"> Disclaimer</a> |
<a href="http://www.wordproject.org/contact/new/state.htm" target="_top">Statement of Faith</a> |
<a href="http://www.wordproject.org/contact/new/mstate.htm" target="_top">Mission</a> |
<a href="http://www.wordproject.org/contact/new/copyrights.htm" target="_top">Copyrights</a></p>
</div>
</div>
</footer>
</body>
</script><script type="text/javascript" src="../_assets/js/jquery-1.8.0.min.js"></script>
<script type="text/javascript" src="../_assets/js/soundmanager2.js"></script>
<script type="text/javascript" src="../_assets/js/page-player.js"></script>
<script type="text/javascript" src="../_assets/js/script.js"></script>
<script type="text/javascript">
soundManager.setup({
url: '../_assets/swf/'
});
</script>
</html>

View File

@ -0,0 +1,20 @@
"1","النعمة المذهلة",,"النعمة المذهلة
كم هو شديَ ذلك الصوت
الذي أنقذ شخصا تعيسا مثلي !
كنت يوما ضائعا ، أما الآن فقد وُجدت
كنت أعمى ، أما الآن فأنا أرى
إن النعمة هي التي علمت قلبي الخوف
وإن النعمة هي التي أزالت مخاوفي
كم بدت لي تلك النعمة ثمينة
ساعة ما آمنت لأول مرة
بكثير من المخاطر والمحن والفتن مررت
وها أنا ذا قد اجتزتها
إن النعمة هي التي ساقتني آمنا حتى هنا
وإن النعمة هي التي ستقودني حتى البيت
حتى لو مكثنا هنا عشرة ألف عام
نشع بمثل سطوع الشمس
لن ينقص ذلك من الأيام التي علينا أن نغني فيها تمجيدا للرب
ولا يوما واحدا منذ ابتدينا","John Newton","Public Domain",,
1 1 النعمة المذهلة النعمة المذهلة كم هو شديَ ذلك الصوت الذي أنقذ شخصا تعيسا مثلي ! كنت يوما ضائعا ، أما الآن فقد وُجدت كنت أعمى ، أما الآن فأنا أرى إن النعمة هي التي علمت قلبي الخوف وإن النعمة هي التي أزالت مخاوفي كم بدت لي تلك النعمة ثمينة ساعة ما آمنت لأول مرة بكثير من المخاطر والمحن والفتن مررت وها أنا ذا قد اجتزتها إن النعمة هي التي ساقتني آمنا حتى هنا وإن النعمة هي التي ستقودني حتى البيت حتى لو مكثنا هنا عشرة ألف عام نشع بمثل سطوع الشمس لن ينقص ذلك من الأيام التي علينا أن نغني فيها تمجيدا للرب ولا يوما واحدا منذ ابتدينا John Newton Public Domain

View File

@ -0,0 +1,26 @@
{
"authors": [
"John Newton"
],
"copyright": "Public Domain",
"title": "النعمة المذهلة",
"verse_order_list": [],
"verses": [
[
"النعمة المذهلة\nكم هو شديَ ذلك الصوت\nالذي أنقذ شخصا تعيسا مثلي !\nكنت يوما ضائعا ، أما الآن فقد وُجدت\nكنت أعمى ، أما الآن فأنا أرى\n",
"v"
],
[
"إن النعمة هي التي علمت قلبي الخوف\nوإن النعمة هي التي أزالت مخاوفي\nكم بدت لي تلك النعمة ثمينة\nساعة ما آمنت لأول مرة\n",
"v"
],
[
"بكثير من المخاطر والمحن والفتن مررت\nوها أنا ذا قد اجتزتها\nإن النعمة هي التي ساقتني آمنا حتى هنا\nوإن النعمة هي التي ستقودني حتى البيت\n",
"v"
],
[
"حتى لو مكثنا هنا عشرة ألف عام\nنشع بمثل سطوع الشمس\nلن ينقص ذلك من الأيام التي علينا أن نغني فيها تمجيدا للرب\nولا يوما واحدا منذ ابتدينا\n",
"v"
]
]
}