mirror of https://gitlab.com/openlp/openlp.git
Merge branch 'master' into list-view
This commit is contained in:
commit
a22284fd0c
|
@ -1,3 +1,25 @@
|
|||
OpenLP 3.0.2
|
||||
============
|
||||
|
||||
* Only show hash if song book number exists
|
||||
* FIX: Missing looping for theme background videos
|
||||
* Fixing Songs' Topics media manager icon to be the same from the Song Maintenance dialog
|
||||
* Adding ability to return transposed item with service_item format to avoid duplicate calls on remote
|
||||
* Fix OpenLyrics whitespaces being 'eaten' (again)
|
||||
* Fixing service manager's list exception when pressing 'Left' keyboard key without any item selected
|
||||
* Force the use of SqlAlchemy 1.4 for now
|
||||
* Removing login requirement from transpose endpoint
|
||||
* Handle verse ranges in BibleServer
|
||||
* Fix up loading 2.9.x services
|
||||
* Attempt to fix #1287 by checking for both str and bytes, and decoding bytes to unicode
|
||||
* Add debugging for VLC and fix strange state.
|
||||
* Display the closing progress dialog during plugin shutdown
|
||||
* Fix an issue with the Worship Center Pro importer
|
||||
* Fix white preview display when previewing presentations
|
||||
* Fix an issue where the websockets server would try to shut down even when -w is supplied
|
||||
* Use a simpler approach when creating a tmp file when saving service files
|
||||
|
||||
|
||||
OpenLP 2.5.1
|
||||
============
|
||||
|
||||
|
|
|
@ -33,8 +33,8 @@ init:
|
|||
install:
|
||||
# Update pip
|
||||
- python -m pip install --upgrade pip
|
||||
# Install generic dependencies from pypi
|
||||
- python -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc zeroconf flask-cors pytest-qt pyenchant pysword qrcode pillow
|
||||
# Install generic dependencies from pypi. sqlalchemy most be 1.4 for now
|
||||
- python -m pip install "sqlalchemy<1.5" alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc zeroconf flask-cors pytest-qt pyenchant pysword qrcode pillow
|
||||
# Install Windows only dependencies
|
||||
- cmd: python -m pip install pyodbc pypiwin32
|
||||
- cmd: choco install vlc %CHOCO_VLC_ARG% --no-progress --limit-output
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.0.0.dev6+dbea8bf56
|
||||
3.0.2
|
||||
|
|
|
@ -59,6 +59,8 @@ def system_information():
|
|||
data = {}
|
||||
data['websocket_port'] = Registry().get('settings_thread').value('api/websocket port')
|
||||
data['login_required'] = Registry().get('settings_thread').value('api/authentication enabled')
|
||||
data['api_version'] = 2
|
||||
data['api_revision'] = 2
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
|
|
|
@ -21,11 +21,13 @@
|
|||
##########################################################################
|
||||
import logging
|
||||
|
||||
import json
|
||||
import re
|
||||
from flask import abort, request, Blueprint, jsonify
|
||||
from flask import abort, request, Blueprint, jsonify, Response
|
||||
|
||||
from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth
|
||||
from openlp.core.lib.plugin import PluginStatus
|
||||
from openlp.core.common.json import OpenLPJSONEncoder
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.plugins.songs.lib import transpose_lyrics
|
||||
|
||||
|
@ -141,9 +143,10 @@ def set_search_option(plugin):
|
|||
|
||||
|
||||
@plugins.route('/songs/transpose-live-item/<transpose_value>', methods=['GET'])
|
||||
@login_required
|
||||
def transpose(transpose_value):
|
||||
log.debug('songs/transpose-live-item called')
|
||||
response_format = request.args.get('response_format', None, type=str)
|
||||
return_service_item = response_format == 'service_item'
|
||||
if transpose_value:
|
||||
try:
|
||||
transpose_value = int(transpose_value)
|
||||
|
@ -170,9 +173,19 @@ def transpose(transpose_value):
|
|||
verse_list = re.split(r'---\[Verse:(.+?)\]---', transposed_lyrics)
|
||||
# remove first blank entry
|
||||
verse_list = verse_list[1:]
|
||||
j = 0
|
||||
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
|
||||
if return_service_item:
|
||||
live_item['slides'][j]['chords'] = verse_list[i + 1].strip()
|
||||
j += 1
|
||||
else:
|
||||
chord_slides.append({'chords': verse_list[i + 1].strip(), 'verse': verse_list[i]})
|
||||
if return_service_item:
|
||||
live_item['chords_transposed'] = True
|
||||
json_live_item = json.dumps(live_item, cls=OpenLPJSONEncoder)
|
||||
return Response(json_live_item, mimetype='application/json')
|
||||
else:
|
||||
return jsonify(chord_slides), 200
|
||||
abort(400)
|
||||
|
||||
|
||||
|
|
|
@ -205,6 +205,8 @@ class WebSocketServer(RegistryBase, RegistryProperties, QtCore.QObject, LogMixin
|
|||
"""
|
||||
Closes the WebSocket server and detach associated signals
|
||||
"""
|
||||
if Registry().get_flag('no_web_server'):
|
||||
return
|
||||
try:
|
||||
poller.poller_changed.disconnect(self.handle_poller_signal)
|
||||
poller.unhook_signals()
|
||||
|
|
|
@ -30,7 +30,7 @@ from openlp.core.common.registry import Registry
|
|||
|
||||
|
||||
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
|
||||
'preview_size_changed', 'resizeEvent', 'eventFilter', 'tick']
|
||||
'preview_size_changed', 'resizeEvent', 'eventFilter', 'tick', 'resize', 'update_ui']
|
||||
|
||||
|
||||
class LogMixin(object):
|
||||
|
|
|
@ -774,7 +774,10 @@ class Settings(QtCore.QSettings):
|
|||
# An empty dictionary saved to the settings results in a None type being returned.
|
||||
elif isinstance(default_value, dict):
|
||||
return {}
|
||||
elif isinstance(setting, str):
|
||||
elif isinstance(setting, (str, bytes)):
|
||||
if isinstance(setting, bytes):
|
||||
# convert to str
|
||||
setting = setting.decode('utf8')
|
||||
if 'json_meta' in setting or '__Path__' in setting or setting.startswith('{'):
|
||||
return json.loads(setting, cls=OpenLPJSONDecoder)
|
||||
# Convert the setting to the correct type.
|
||||
|
|
|
@ -474,7 +474,7 @@ var Display = {
|
|||
img.setAttribute("style", "height: 100%; width: 100%");
|
||||
section.appendChild(img);
|
||||
Display._slides['0'] = 0;
|
||||
Display.replaceSlides(parentSection);
|
||||
Display.replaceSlides(section);
|
||||
},
|
||||
/**
|
||||
* Set fullscreen image from base64 data
|
||||
|
@ -932,7 +932,7 @@ var Display = {
|
|||
/*
|
||||
Disabling all transitions (except body) to allow the Webview to attain the
|
||||
transparent state before it gets hidden by Qt.
|
||||
*/
|
||||
*/
|
||||
document.body.classList.add('disable-transitions');
|
||||
document.body.classList.add('is-desktop');
|
||||
Display._slidesContainer.style.opacity = 0;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets, Qt
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
|
||||
|
@ -54,7 +54,7 @@ class Ui_ConfirmationDialog():
|
|||
self.listview = QtWidgets.QListView(self)
|
||||
self.listview.setObjectName("confirmation listview")
|
||||
# make the entries read-only
|
||||
self.listview.setEditTriggers(Qt.QAbstractItemView.NoEditTriggers)
|
||||
self.listview.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.confirmation_layout.addWidget(self.listview)
|
||||
|
||||
# add the items to the listview model
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
This is the main window, where all the action happens.
|
||||
"""
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
@ -529,20 +530,32 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||
self.ws_server = WebSocketServer()
|
||||
self.screen_updating_lock = Lock()
|
||||
|
||||
@contextmanager
|
||||
def _show_wait_dialog(self, title, message):
|
||||
"""
|
||||
Show a wait dialog, wait for some tasks to complete, and then close it.
|
||||
"""
|
||||
try:
|
||||
# Display a progress dialog with a message
|
||||
wait_dialog = QtWidgets.QProgressDialog(message, '', 0, 0, self)
|
||||
wait_dialog.setWindowTitle(title)
|
||||
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
|
||||
wait_dialog.setWindowFlag(window_flag, False)
|
||||
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
wait_dialog.setAutoClose(False)
|
||||
wait_dialog.setCancelButton(None)
|
||||
wait_dialog.show()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
yield
|
||||
finally:
|
||||
# Finally close the message window
|
||||
wait_dialog.close()
|
||||
|
||||
def _wait_for_threads(self):
|
||||
"""
|
||||
Wait for the threads
|
||||
"""
|
||||
# Sometimes the threads haven't finished, let's wait for them
|
||||
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
|
||||
'', 0, 0, self)
|
||||
wait_dialog.setWindowTitle(translate('OpenLP.MainWindow', 'Please Wait'))
|
||||
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
|
||||
wait_dialog.setWindowFlag(window_flag, False)
|
||||
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
wait_dialog.setAutoClose(False)
|
||||
wait_dialog.setCancelButton(None)
|
||||
wait_dialog.show()
|
||||
thread_names = list(self.application.worker_threads.keys())
|
||||
for thread_name in thread_names:
|
||||
if thread_name not in self.application.worker_threads.keys():
|
||||
|
@ -569,7 +582,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||
except RuntimeError:
|
||||
# Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
|
||||
pass
|
||||
wait_dialog.close()
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
"""
|
||||
|
@ -1085,10 +1097,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||
else:
|
||||
event.accept()
|
||||
if event.isAccepted():
|
||||
# Wait for all the threads to complete
|
||||
self._wait_for_threads()
|
||||
# If we just did a settings import, close without saving changes.
|
||||
self.clean_up(save_settings=not self.settings_imported)
|
||||
with self._show_wait_dialog(translate('OpenLP.MainWindow', 'Please Wait'),
|
||||
translate('OpenLP.MainWindow', 'Waiting for some things to finish...')):
|
||||
# Wait for all the threads to complete
|
||||
self._wait_for_threads()
|
||||
# If we just did a settings import, close without saving changes.
|
||||
self.clean_up(save_settings=not self.settings_imported)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.FileOpen:
|
||||
|
|
|
@ -497,7 +497,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
|||
if controller.media_info.is_playing and controller.media_info.length > 0:
|
||||
controller.media_info.timer += TICK_TIME
|
||||
if controller.media_info.timer >= controller.media_info.start_time + controller.media_info.length:
|
||||
if is_looping_playback(controller):
|
||||
if is_looping_playback(controller) or self.is_theme_background:
|
||||
start_again = True
|
||||
else:
|
||||
self.media_stop(controller)
|
||||
|
@ -683,6 +683,9 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
|
|||
else:
|
||||
self._media_set_visibility(controller, False)
|
||||
del self.current_media_players[controller.controller_type]
|
||||
controller.mediabar.actions['playbackPlay'].setVisible(True)
|
||||
controller.mediabar.actions['playbackStop'].setDisabled(True)
|
||||
controller.mediabar.actions['playbackPause'].setVisible(False)
|
||||
|
||||
def media_hide_msg(self, msg):
|
||||
"""
|
||||
|
|
|
@ -32,6 +32,7 @@ from time import sleep
|
|||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import LogMixin
|
||||
from openlp.core.common.platform import is_linux, is_macosx, is_win
|
||||
from openlp.core.display.screens import ScreenList
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
|
@ -83,7 +84,7 @@ if is_linux() and 'pytest' not in sys.argv[0] and get_vlc():
|
|||
log.exception('Failed to run XInitThreads(), VLC might not work properly!')
|
||||
|
||||
|
||||
class VlcPlayer(MediaPlayer):
|
||||
class VlcPlayer(MediaPlayer, LogMixin):
|
||||
"""
|
||||
A specialised version of the MediaPlayer class, which provides a VLC display.
|
||||
"""
|
||||
|
@ -133,7 +134,7 @@ class VlcPlayer(MediaPlayer):
|
|||
controller.vlc_instance = vlc.Instance(command_line_options)
|
||||
if not controller.vlc_instance:
|
||||
return
|
||||
log.debug(f"VLC version: {vlc.libvlc_get_version()}")
|
||||
self.log_debug(f"VLC version: {vlc.libvlc_get_version()}")
|
||||
# creating an empty vlc media player
|
||||
controller.vlc_media_player = controller.vlc_instance.media_player_new()
|
||||
controller.vlc_widget.resize(controller.size())
|
||||
|
@ -172,7 +173,7 @@ class VlcPlayer(MediaPlayer):
|
|||
"""
|
||||
if not controller.vlc_instance:
|
||||
return False
|
||||
log.debug('load video in VLC Controller')
|
||||
self.log_debug('load video in VLC Controller')
|
||||
path = None
|
||||
if file and not controller.media_info.media_type == MediaType.Stream:
|
||||
path = os.path.normcase(file)
|
||||
|
@ -201,7 +202,7 @@ class VlcPlayer(MediaPlayer):
|
|||
path = '/' + path
|
||||
dvd_location = 'dvd://' + path + '#' + controller.media_info.title_track
|
||||
controller.vlc_media = controller.vlc_instance.media_new_location(dvd_location)
|
||||
log.debug(f"vlc dvd load: {dvd_location}")
|
||||
self.log_debug(f"vlc dvd load: {dvd_location}")
|
||||
controller.vlc_media.add_option(f"start-time={int(controller.media_info.start_time // 1000)}")
|
||||
controller.vlc_media.add_option(f"stop-time={int(controller.media_info.end_time // 1000)}")
|
||||
controller.vlc_media_player.set_media(controller.vlc_media)
|
||||
|
@ -210,10 +211,11 @@ class VlcPlayer(MediaPlayer):
|
|||
self.media_state_wait(controller, VlCState.Playing)
|
||||
if controller.media_info.audio_track > 0:
|
||||
res = controller.vlc_media_player.audio_set_track(controller.media_info.audio_track)
|
||||
log.debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track) + ' ' + str(res))
|
||||
self.log_debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track) + ' ' + str(res))
|
||||
if controller.media_info.subtitle_track > 0:
|
||||
res = controller.vlc_media_player.video_set_spu(controller.media_info.subtitle_track)
|
||||
log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track) + ' ' + str(res))
|
||||
self.log_debug('vlc play, subtitle_track set: ' +
|
||||
str(controller.media_info.subtitle_track) + ' ' + str(res))
|
||||
elif controller.media_info.media_type == MediaType.Stream:
|
||||
controller.vlc_media = controller.vlc_instance.media_new_location(file[0])
|
||||
controller.vlc_media.add_options(file[1])
|
||||
|
@ -269,7 +271,7 @@ class VlcPlayer(MediaPlayer):
|
|||
:param output_display: The display where the media is
|
||||
:return:
|
||||
"""
|
||||
log.debug('vlc play, mediatype: ' + str(controller.media_info.media_type))
|
||||
self.log_debug('vlc play, mediatype: ' + str(controller.media_info.media_type))
|
||||
threading.Thread(target=controller.vlc_media_player.play).start()
|
||||
if not self.media_state_wait(controller, VlCState.Playing):
|
||||
return False
|
||||
|
|
|
@ -29,7 +29,6 @@ import zipfile
|
|||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
@ -690,8 +689,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
self.log_debug('ServiceManager.save_file - ZIP contents size is %i bytes' % total_size)
|
||||
self.main_window.display_progress_bar(1000)
|
||||
try:
|
||||
with NamedTemporaryFile(dir=str(file_path.parent), prefix='.') as temp_file, \
|
||||
zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
tmp_file_path = str(file_path) + ".saving"
|
||||
with zipfile.ZipFile(tmp_file_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
# First we add service contents..
|
||||
zip_file.writestr('service_data.osj', service_content)
|
||||
self.main_window.increment_progress_bar(service_content_size / total_size * 1000)
|
||||
|
@ -701,11 +700,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
self.main_window.increment_progress_bar(local_file_item.stat().st_size / total_size * 1000)
|
||||
with suppress(FileNotFoundError):
|
||||
file_path.unlink()
|
||||
# Try to link rather than copy to prevent writing another file
|
||||
try:
|
||||
os.link(temp_file.name, file_path)
|
||||
except OSError:
|
||||
shutil.copyfile(temp_file.name, file_path)
|
||||
# Move rather than copy to prevent writing another file if possible
|
||||
shutil.move(tmp_file_path, file_path)
|
||||
self.settings.setValue('servicemanager/last directory', file_path.parent)
|
||||
except (PermissionError, OSError) as error:
|
||||
self.log_exception('Failed to save service to disk: {name}'.format(name=file_path))
|
||||
|
@ -795,6 +791,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
# if the filename, directory name, or volume label syntax is incorrect it can cause an exception
|
||||
return False
|
||||
service_data = None
|
||||
is_broken_file = False
|
||||
self.application.set_busy_cursor()
|
||||
try:
|
||||
# TODO: figure out a way to use the presentation thumbnails from the service file
|
||||
|
@ -832,6 +829,13 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
# into the root of the service folder.
|
||||
if self.servicefile_version and self.servicefile_version < 3:
|
||||
zip_info.filename = os.path.basename(zip_info.filename.replace('/', os.path.sep))
|
||||
else:
|
||||
# Fix an issue with service files created in 2.9.x
|
||||
fname, ext = os.path.splitext(zip_info.filename)
|
||||
if not ext and os.path.basename(fname).startswith('.'):
|
||||
zip_info.filename = fname.replace('/', '')
|
||||
is_broken_file = True
|
||||
self.log_debug(f'Fixing file {fname} => {zip_info.filename}')
|
||||
zip_file.extract(zip_info, str(self.service_path))
|
||||
self.main_window.increment_progress_bar(zip_info.compress_size)
|
||||
# Handle the content
|
||||
|
@ -840,6 +844,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
self.set_file_name(file_path)
|
||||
self.main_window.add_recent_file(file_path)
|
||||
self.set_modified(False)
|
||||
# If this file was broken due to a bug in 2.9.x, save the file to fix it.
|
||||
if is_broken_file:
|
||||
self.save_file()
|
||||
self.settings.setValue('servicemanager/last file', file_path)
|
||||
except (NameError, OSError, ValidationError, zipfile.BadZipFile):
|
||||
self.application.set_normal_cursor()
|
||||
|
@ -1197,15 +1204,16 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||
Collapses cursor selection on the window Called by the left arrow
|
||||
"""
|
||||
item = self.service_manager_list.currentItem()
|
||||
# Since we only have 2 levels we find them by checking for children
|
||||
if item.childCount():
|
||||
if self.service_manager_list.isExpanded(self.service_manager_list.currentIndex()):
|
||||
self.service_manager_list.collapseItem(item)
|
||||
self.service_manager.collapsed(item)
|
||||
else: # If selection is lower level
|
||||
self.service_manager_list.collapseItem(item.parent())
|
||||
self.service_manager.collapsed(item.parent())
|
||||
self.service_manager_list.setCurrentItem(item.parent())
|
||||
if item is not None:
|
||||
# Since we only have 2 levels we find them by checking for children
|
||||
if item.childCount():
|
||||
if self.service_manager_list.isExpanded(self.service_manager_list.currentIndex()):
|
||||
self.service_manager_list.collapseItem(item)
|
||||
self.service_manager.collapsed(item)
|
||||
else: # If selection is lower level
|
||||
self.service_manager_list.collapseItem(item.parent())
|
||||
self.service_manager.collapsed(item.parent())
|
||||
self.service_manager_list.setCurrentItem(item.parent())
|
||||
|
||||
def on_collapse_all(self):
|
||||
"""
|
||||
|
|
|
@ -453,7 +453,11 @@ class BSExtract(RegistryProperties):
|
|||
verses = {}
|
||||
for verse in content:
|
||||
self.application.process_events()
|
||||
versenumber = int(verse.find('span', 'verse-number__group').get_text().strip())
|
||||
versenumber = verse.find('span', 'verse-number__group').get_text().strip()
|
||||
if '-' in versenumber:
|
||||
# Some translations bundle verses together, see https://gitlab.com/openlp/openlp/-/issues/1104
|
||||
versenumber = versenumber.split('-')[0]
|
||||
versenumber = int(versenumber)
|
||||
verses[versenumber] = verse.find('span', 'verse-content--hover').get_text().strip()
|
||||
return SearchResults(book_name, chapter, verses)
|
||||
|
||||
|
|
|
@ -84,11 +84,32 @@ class OpenLyricsImport(SongImport):
|
|||
"""
|
||||
Remove leading and trailing whitespace from the 'text' and 'tail' attributes of an etree._Element object
|
||||
"""
|
||||
is_chord_after_tail = False
|
||||
if next_subelem is not None:
|
||||
# Trimming whitespaces after br tags.
|
||||
# We can't trim the spaces before 'chord' tags
|
||||
is_chord_after_tail = False
|
||||
if next_subelem.tag.endswith('chord'):
|
||||
is_chord_after_tail = True
|
||||
if elem.text is not None:
|
||||
elem.text = elem.text.strip()
|
||||
if elem.tail is not None and not is_chord_after_tail:
|
||||
elem.tail = elem.tail.strip()
|
||||
if elem.text is not None:
|
||||
elem.text = elem.text.strip()
|
||||
if elem.tail is not None and not is_chord_after_tail:
|
||||
elem.tail = elem.tail.strip()
|
||||
else:
|
||||
has_children = bool(elem.getchildren())
|
||||
if elem.text is not None:
|
||||
if has_children:
|
||||
# Can only strip on left as there's children inside
|
||||
elem.text = elem.text.lstrip()
|
||||
else:
|
||||
elem.text = elem.text.strip()
|
||||
if elem.tail is not None:
|
||||
if has_children:
|
||||
# Can only strip on right as there's children inside
|
||||
elem.tail = elem.tail.rstrip()
|
||||
else:
|
||||
elem.tail = elem.tail.strip()
|
||||
elif has_children:
|
||||
# Can only strip on right as it's the last tag and there's no tail
|
||||
last_elem = elem.getchildren()[-1]
|
||||
if last_elem.tail is not None:
|
||||
last_elem.tail = last_elem.tail.rstrip()
|
||||
|
|
|
@ -94,20 +94,18 @@ class WorshipCenterProImport(SongImport):
|
|||
marker_end = verse.find('>')
|
||||
marker = verse[marker_start + 1:marker_end]
|
||||
# Identify the marker type
|
||||
if 'REFRAIN' in marker or 'CHORUS' in marker:
|
||||
if marker in ['REFRAIN', 'CHORUS']:
|
||||
marker_type = 'c'
|
||||
elif 'BRIDGE' in marker:
|
||||
elif marker == 'BRIDGE':
|
||||
marker_type = 'b'
|
||||
elif 'PRECHORUS' in marker:
|
||||
elif marker == 'PRECHORUS':
|
||||
marker_type = 'p'
|
||||
elif 'END' in marker:
|
||||
elif marker == 'END':
|
||||
marker_type = 'e'
|
||||
elif 'INTRO' in marker:
|
||||
elif marker == 'INTRO':
|
||||
marker_type = 'i'
|
||||
elif 'TAG' in marker:
|
||||
elif marker == 'TAG':
|
||||
marker_type = 'o'
|
||||
else:
|
||||
marker_type = 'v'
|
||||
# Strip tags from text
|
||||
verse = re.sub('<[^<]+?>', '', verse)
|
||||
self.add_verse(verse.strip(), marker_type)
|
||||
|
|
|
@ -145,7 +145,7 @@ class SongMediaItem(MediaManagerItem):
|
|||
translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
|
||||
(SongSearch.Authors, UiIcons().user, SongStrings.Authors,
|
||||
translate('SongsPlugin.MediaItem', 'Search Authors...')),
|
||||
(SongSearch.Topics, UiIcons().theme, SongStrings.Topics,
|
||||
(SongSearch.Topics, UiIcons().light_bulb, SongStrings.Topics,
|
||||
translate('SongsPlugin.MediaItem', 'Search Topics...')),
|
||||
(SongSearch.Books, UiIcons().address, SongStrings.SongBooks,
|
||||
translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
|
||||
|
@ -568,8 +568,11 @@ class SongMediaItem(MediaManagerItem):
|
|||
if self.settings.value('songs/add songbook slide') and song.songbook_entries:
|
||||
first_slide = '\n'
|
||||
for songbook_entry in song.songbook_entries:
|
||||
first_slide += '{book} #{num}'.format(book=songbook_entry.songbook.name,
|
||||
num=songbook_entry.entry)
|
||||
if songbook_entry.entry:
|
||||
first_slide += '{book} #{num}'.format(book=songbook_entry.songbook.name,
|
||||
num=songbook_entry.entry)
|
||||
else:
|
||||
first_slide += songbook_entry.songbook.name
|
||||
if songbook_entry.songbook.publisher:
|
||||
first_slide += ' ({pub})'.format(pub=songbook_entry.songbook.publisher)
|
||||
first_slide += '\n\n'
|
||||
|
|
1176
resources/i18n/af.ts
1176
resources/i18n/af.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/bg.ts
1176
resources/i18n/bg.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/cs.ts
1176
resources/i18n/cs.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/da.ts
1176
resources/i18n/da.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/de.ts
1176
resources/i18n/de.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/el.ts
1176
resources/i18n/el.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1253
resources/i18n/es.ts
1253
resources/i18n/es.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/et.ts
1176
resources/i18n/et.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/fi.ts
1176
resources/i18n/fi.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/fr.ts
1176
resources/i18n/fr.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/hu.ts
1176
resources/i18n/hu.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/id.ts
1176
resources/i18n/id.ts
File diff suppressed because it is too large
Load Diff
2675
resources/i18n/it.ts
2675
resources/i18n/it.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/ja.ts
1176
resources/i18n/ja.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/ko.ts
1176
resources/i18n/ko.ts
File diff suppressed because it is too large
Load Diff
1178
resources/i18n/lt.ts
1178
resources/i18n/lt.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/nb.ts
1176
resources/i18n/nb.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/nl.ts
1176
resources/i18n/nl.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/pl.ts
1176
resources/i18n/pl.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/ro.ts
1176
resources/i18n/ro.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/ru.ts
1176
resources/i18n/ru.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/sk.ts
1176
resources/i18n/sk.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/sl.ts
1176
resources/i18n/sl.ts
File diff suppressed because it is too large
Load Diff
1176
resources/i18n/sv.ts
1176
resources/i18n/sv.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2
setup.py
2
setup.py
|
@ -118,7 +118,7 @@ using a computer and a display/projector.""",
|
|||
'QtAwesome',
|
||||
"qrcode",
|
||||
'requests',
|
||||
'SQLAlchemy >= 0.5',
|
||||
'SQLAlchemy < 1.5',
|
||||
'waitress',
|
||||
'WebOb',
|
||||
'websockets',
|
||||
|
|
|
@ -63,7 +63,7 @@ def mocked_qapp():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def registry():
|
||||
def registry(autouse=True):
|
||||
"""An instance of the Registry"""
|
||||
yield Registry.create()
|
||||
Registry._instances = {}
|
||||
|
@ -78,14 +78,14 @@ def settings(qapp, registry):
|
|||
# Needed on windows to make sure a Settings object is available during the tests
|
||||
sets = Settings()
|
||||
sets.setValue('themes/global theme', 'my_theme')
|
||||
Registry().register('settings', sets)
|
||||
Registry().register('settings_thread', sets)
|
||||
Registry().register('application', qapp)
|
||||
registry.register('settings', sets)
|
||||
registry.register('settings_thread', sets)
|
||||
registry.register('application', qapp)
|
||||
qapp.settings = sets
|
||||
yield sets
|
||||
del sets
|
||||
Registry().remove('settings')
|
||||
Registry().remove('settings_thread')
|
||||
registry.remove('settings')
|
||||
registry.remove('settings_thread')
|
||||
os.close(fd)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
|
@ -95,12 +95,12 @@ def mock_settings(qapp, registry):
|
|||
"""A Mock Settings() instance"""
|
||||
# Create and register a mock settings object to work with
|
||||
mk_settings = MagicMock()
|
||||
Registry().register('settings', mk_settings)
|
||||
Registry().register('application', qapp)
|
||||
Registry().register('settings_thread', mk_settings)
|
||||
registry.register('settings', mk_settings)
|
||||
registry.register('application', qapp)
|
||||
registry.register('settings_thread', mk_settings)
|
||||
yield mk_settings
|
||||
Registry().remove('settings')
|
||||
Registry().remove('settings_thread')
|
||||
registry.remove('settings')
|
||||
registry.remove('settings_thread')
|
||||
del mk_settings
|
||||
|
||||
|
||||
|
|
|
@ -346,7 +346,7 @@ describe("Screen Visibility", function () {
|
|||
done();
|
||||
}, TRANSITION_TIMEOUT);
|
||||
});
|
||||
|
||||
|
||||
it("should trigger dispatchEvent when toBlack(eventName) is called with an event parameter", function (done) {
|
||||
var testEventName = 'event_33';
|
||||
displayWatcher.dispatchEvent = function(eventName) {
|
||||
|
@ -959,10 +959,33 @@ describe("Display.setImageSlides", function () {
|
|||
expect(Display._slides["1"]).toEqual(1);
|
||||
expect($(".slides > section > section").length).toEqual(2);
|
||||
expect($(".slides > section > section > img").length).toEqual(2);
|
||||
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
|
||||
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;")
|
||||
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
|
||||
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;")
|
||||
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg");
|
||||
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
|
||||
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg");
|
||||
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
|
||||
expect(Reveal.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Display.setFullscreenImage", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slides_container = _createDiv({"class": "slides"});
|
||||
var footer_container = _createDiv({"class": "footer"});
|
||||
Display._slidesContainer = slides_container;
|
||||
Display._footerContainer = footer_container;
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
it("should set a fullscreen image", function () {
|
||||
var image = "file:///openlp1.jpg";
|
||||
let bg_color = "#000";
|
||||
spyOn(Reveal, "sync");
|
||||
spyOn(Reveal, "slide");
|
||||
|
||||
Display.setFullscreenImage(bg_color, image);
|
||||
|
||||
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg");
|
||||
expect(Reveal.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,42 +41,6 @@ def worker(settings):
|
|||
yield worker
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.WebSocketWorker')
|
||||
@patch('openlp.core.api.websockets.run_thread')
|
||||
def test_serverstart(mocked_run_thread, MockWebSocketWorker, registry):
|
||||
"""
|
||||
Test the starting of the WebSockets Server with the disabled flag set off
|
||||
"""
|
||||
# GIVEN: A new WebSocketServer
|
||||
Registry().set_flag('no_web_server', False)
|
||||
server = WebSocketServer()
|
||||
|
||||
# WHEN: I start the server
|
||||
server.start()
|
||||
|
||||
# THEN: the api environment should have been created
|
||||
assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
|
||||
assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.WebSocketWorker')
|
||||
@patch('openlp.core.api.websockets.run_thread')
|
||||
def test_serverstart_not_required(mocked_run_thread, MockWebSocketWorker, registry):
|
||||
"""
|
||||
Test the starting of the WebSockets Server with the disabled flag set on
|
||||
"""
|
||||
# GIVEN: A new WebSocketServer and the server is not required
|
||||
Registry().set_flag('no_web_server', True)
|
||||
server = WebSocketServer()
|
||||
|
||||
# WHEN: I start the server
|
||||
server.start()
|
||||
|
||||
# THEN: the api environment should have not been created
|
||||
assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
|
||||
assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
|
||||
|
||||
|
||||
def test_poller_get_state(poller, settings):
|
||||
"""
|
||||
Test the get_state function returns the correct JSON
|
||||
|
@ -107,50 +71,6 @@ def test_poller_get_state(poller, settings):
|
|||
assert poll_json['results']['item'] == '23-34-45', 'The item return value should match 23-34-45'
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.serve')
|
||||
@patch('openlp.core.api.websockets.asyncio')
|
||||
@patch('openlp.core.api.websockets.log')
|
||||
def test_worker_start(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
|
||||
"""
|
||||
Test the start function of the worker
|
||||
"""
|
||||
# GIVEN: A mocked serve function and event loop
|
||||
mocked_serve.return_value = 'server_thing'
|
||||
event_loop = MagicMock()
|
||||
mocked_asyncio.new_event_loop.return_value = event_loop
|
||||
# WHEN: The start function is called
|
||||
worker.start()
|
||||
# THEN: No error occurs
|
||||
mocked_serve.assert_called_once()
|
||||
event_loop.run_until_complete.assert_called_once_with('server_thing')
|
||||
event_loop.run_forever.assert_called_once_with()
|
||||
mocked_log.exception.assert_not_called()
|
||||
# Because run_forever is mocked, it doesn't stall the thread so close will be called immediately
|
||||
event_loop.close.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.serve')
|
||||
@patch('openlp.core.api.websockets.asyncio')
|
||||
@patch('openlp.core.api.websockets.log')
|
||||
def test_worker_start_fail(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
|
||||
"""
|
||||
Test the start function of the worker handles a error nicely
|
||||
"""
|
||||
# GIVEN: A mocked serve function and event loop. run_until_complete returns a error
|
||||
mocked_serve.return_value = 'server_thing'
|
||||
event_loop = MagicMock()
|
||||
mocked_asyncio.new_event_loop.return_value = event_loop
|
||||
event_loop.run_until_complete.side_effect = Exception()
|
||||
# WHEN: The start function is called
|
||||
worker.start()
|
||||
# THEN: An exception is logged but is handled and the event_loop is closed
|
||||
mocked_serve.assert_called_once()
|
||||
event_loop.run_until_complete.assert_called_once_with('server_thing')
|
||||
event_loop.run_forever.assert_not_called()
|
||||
mocked_log.exception.assert_called_once()
|
||||
event_loop.close.assert_called_once_with()
|
||||
|
||||
|
||||
def test_poller_event_attach(poller, settings):
|
||||
"""
|
||||
Test the event attach of WebSocketPoller
|
||||
|
@ -206,6 +126,102 @@ def test_poller_get_state_is_never_none(poller):
|
|||
assert state is not None, 'get_state() return should not be None'
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.serve')
|
||||
@patch('openlp.core.api.websockets.asyncio')
|
||||
@patch('openlp.core.api.websockets.log')
|
||||
def test_websocket_worker_start(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
|
||||
"""
|
||||
Test the start function of the worker
|
||||
"""
|
||||
# GIVEN: A mocked serve function and event loop
|
||||
mocked_serve.return_value = 'server_thing'
|
||||
event_loop = MagicMock()
|
||||
mocked_asyncio.new_event_loop.return_value = event_loop
|
||||
# WHEN: The start function is called
|
||||
worker.start()
|
||||
# THEN: No error occurs
|
||||
mocked_serve.assert_called_once()
|
||||
event_loop.run_until_complete.assert_called_once_with('server_thing')
|
||||
event_loop.run_forever.assert_called_once_with()
|
||||
mocked_log.exception.assert_not_called()
|
||||
# Because run_forever is mocked, it doesn't stall the thread so close will be called immediately
|
||||
event_loop.close.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.serve')
|
||||
@patch('openlp.core.api.websockets.asyncio')
|
||||
@patch('openlp.core.api.websockets.log')
|
||||
def test_websocket_worker_start_fail(mocked_log, mocked_asyncio, mocked_serve, worker, settings):
|
||||
"""
|
||||
Test the start function of the worker handles a error nicely
|
||||
"""
|
||||
# GIVEN: A mocked serve function and event loop. run_until_complete returns a error
|
||||
mocked_serve.return_value = 'server_thing'
|
||||
event_loop = MagicMock()
|
||||
mocked_asyncio.new_event_loop.return_value = event_loop
|
||||
event_loop.run_until_complete.side_effect = Exception()
|
||||
# WHEN: The start function is called
|
||||
worker.start()
|
||||
# THEN: An exception is logged but is handled and the event_loop is closed
|
||||
mocked_serve.assert_called_once()
|
||||
event_loop.run_until_complete.assert_called_once_with('server_thing')
|
||||
event_loop.run_forever.assert_not_called()
|
||||
mocked_log.exception.assert_called_once()
|
||||
event_loop.close.assert_called_once_with()
|
||||
|
||||
|
||||
def test_websocket_server_bootstrap_post_set_up(settings):
|
||||
"""
|
||||
Test that the bootstrap_post_set_up() method calls the start method
|
||||
"""
|
||||
# GIVEN: A WebSocketServer with the start() method mocked out
|
||||
Registry().set_flag('no_web_server', False)
|
||||
server = WebSocketServer()
|
||||
server.start = MagicMock()
|
||||
|
||||
# WHEN: bootstrap_post_set_up() is called
|
||||
server.bootstrap_post_set_up()
|
||||
|
||||
# THEN: start() should have been called
|
||||
server.start.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.WebSocketWorker')
|
||||
@patch('openlp.core.api.websockets.run_thread')
|
||||
def test_websocket_server_start(mocked_run_thread, MockWebSocketWorker, registry):
|
||||
"""
|
||||
Test the starting of the WebSockets Server with the disabled flag set off
|
||||
"""
|
||||
# GIVEN: A new WebSocketServer
|
||||
Registry().set_flag('no_web_server', False)
|
||||
server = WebSocketServer()
|
||||
|
||||
# WHEN: I start the server
|
||||
server.start()
|
||||
|
||||
# THEN: the api environment should have been created
|
||||
assert mocked_run_thread.call_count == 1, 'The qthread should have been called once'
|
||||
assert MockWebSocketWorker.call_count == 1, 'The http thread should have been called once'
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.WebSocketWorker')
|
||||
@patch('openlp.core.api.websockets.run_thread')
|
||||
def test_websocket_server_start_not_required(mocked_run_thread, MockWebSocketWorker, registry):
|
||||
"""
|
||||
Test the starting of the WebSockets Server with the disabled flag set on
|
||||
"""
|
||||
# GIVEN: A new WebSocketServer and the server is not required
|
||||
Registry().set_flag('no_web_server', True)
|
||||
server = WebSocketServer()
|
||||
|
||||
# WHEN: I start the server
|
||||
server.start()
|
||||
|
||||
# THEN: the api environment should have not been created
|
||||
assert mocked_run_thread.call_count == 0, 'The qthread should not have been called'
|
||||
assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called'
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.poller')
|
||||
@patch('openlp.core.api.websockets.run_thread')
|
||||
def test_websocket_server_connects_to_poller(mock_run_thread, mock_poller, settings):
|
||||
|
@ -245,3 +261,65 @@ def test_websocket_worker_register_connections(mock_run_thread, mock_add_state_t
|
|||
|
||||
# THEN: WebSocketWorker state notify function should be called
|
||||
mock_add_state_to_queues.assert_called_once_with(mock_state)
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.poller')
|
||||
@patch('openlp.core.api.websockets.log')
|
||||
def test_websocket_server_try_poller_hook_signals(mocked_log, mock_poller, settings):
|
||||
"""
|
||||
Test if the websocket_server invokes poller.hook_signals
|
||||
"""
|
||||
# GIVEN: A mocked poller signal and a server
|
||||
mock_poller.hook_signals.side_effect = Exception
|
||||
Registry().set_flag('no_web_server', False)
|
||||
server = WebSocketServer()
|
||||
|
||||
# WHEN: WebSocketServer is started
|
||||
server.try_poller_hook_signals()
|
||||
|
||||
# THEN: poller_changed should be connected with WebSocketServer and correct handler
|
||||
mock_poller.hook_signals.assert_called_once_with()
|
||||
mocked_log.error.assert_called_once_with('Failed to hook poller signals!')
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.poller')
|
||||
def test_websocket_server_close(mock_poller, settings):
|
||||
"""
|
||||
Test that the websocket_server close method works correctly
|
||||
"""
|
||||
# GIVEN: A mocked poller signal and a server
|
||||
Registry().set_flag('no_web_server', False)
|
||||
mock_poller.poller_changed = MagicMock()
|
||||
mock_poller.poller_changed.connect = MagicMock()
|
||||
server = WebSocketServer()
|
||||
server.handle_poller_signal = MagicMock()
|
||||
mock_worker = MagicMock()
|
||||
server.worker = mock_worker
|
||||
|
||||
# WHEN: WebSocketServer is started
|
||||
server.close()
|
||||
|
||||
# THEN: poller_changed should be connected with WebSocketServer and correct handler
|
||||
mock_poller.poller_changed.disconnect.assert_called_once_with(server.handle_poller_signal)
|
||||
mock_poller.unhook_signals.assert_called_once_with()
|
||||
mock_worker.stop.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.api.websockets.poller')
|
||||
def test_websocket_server_close_when_disabled(mock_poller, registry, settings):
|
||||
"""
|
||||
Test if the websocket_server close method correctly skips teardown when disabled
|
||||
"""
|
||||
# GIVEN: A mocked poller signal and a server
|
||||
Registry().set_flag('no_web_server', True)
|
||||
mock_poller.poller_changed = MagicMock()
|
||||
mock_poller.poller_changed.connect = MagicMock()
|
||||
server = WebSocketServer()
|
||||
server.handle_poller_signal = MagicMock()
|
||||
|
||||
# WHEN: WebSocketServer is started
|
||||
server.close()
|
||||
|
||||
# THEN: poller_changed should be connected with WebSocketServer and correct handler
|
||||
assert mock_poller.poller_changed.disconnect.call_count == 0
|
||||
assert mock_poller.unhook_signals.call_count == 0
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
@ -102,14 +103,10 @@ def test_plugin_songs_transpose_returns_plugin_exception(flask_client, settings)
|
|||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_plugin_songs_transpose_wont_call_renderer(flask_client, settings):
|
||||
"""
|
||||
Tests whether the transpose endpoint won't tries to use any Renderer method; the endpoint needs to operate using
|
||||
already-primed caches (as that's what the /live-item endpoint does); also using Renderer from outside the Qt loop
|
||||
causes it to crash.
|
||||
TransposeMockReturn = namedtuple('TransposeMockReturn', ['renderer_mock_any_attr'])
|
||||
|
||||
See https://gitlab.com/openlp/openlp/-/merge_requests/516 for some background on this.
|
||||
"""
|
||||
|
||||
def _init_transpose_mocks():
|
||||
# GIVEN: A mocked plugin_manager, live_controller, renderer and a real service item with the internal slide cache
|
||||
# filled
|
||||
Registry().register('plugin_manager', MagicMock())
|
||||
|
@ -139,8 +136,38 @@ def test_plugin_songs_transpose_wont_call_renderer(flask_client, settings):
|
|||
renderer_mock_any_attr.reset_mock()
|
||||
renderer_mock.format_slides.reset_mock()
|
||||
|
||||
return TransposeMockReturn(renderer_mock_any_attr=renderer_mock_any_attr)
|
||||
|
||||
|
||||
def test_plugin_songs_transpose_wont_call_renderer(flask_client, settings):
|
||||
"""
|
||||
Tests whether the transpose endpoint won't tries to use any Renderer method; the endpoint needs to operate using
|
||||
already-primed caches (as that's what the /live-item endpoint does); also using Renderer from outside the Qt loop
|
||||
causes it to crash.
|
||||
|
||||
See https://gitlab.com/openlp/openlp/-/merge_requests/516 for some background on this.
|
||||
"""
|
||||
# GIVEN: The default mocks for Transpose API
|
||||
mocks = _init_transpose_mocks()
|
||||
|
||||
# WHEN: The endpoint is called
|
||||
flask_client.get('/api/v2/plugins/songs/transpose-live-item/-1')
|
||||
|
||||
# THEN: The renderer should not be called
|
||||
renderer_mock_any_attr.assert_not_called()
|
||||
mocks.renderer_mock_any_attr.assert_not_called()
|
||||
|
||||
|
||||
def test_plugin_songs_transpose_accepts_response_format_service_item(flask_client, settings):
|
||||
"""
|
||||
Tests whether the transpose's return_service_item parameter works
|
||||
"""
|
||||
|
||||
# GIVEN: The default mocks for Transpose API and the default response
|
||||
_init_transpose_mocks()
|
||||
|
||||
# WHEN: The transpose action returning service_item is called
|
||||
service_item_res = flask_client.get('/api/v2/plugins/songs/transpose-live-item/-1?response_format=service_item')
|
||||
|
||||
# THEN: The service item response shouldn't match normal response and should be a service_item response
|
||||
response = service_item_res.json
|
||||
assert 'capabilities' in response
|
||||
|
|
|
@ -141,6 +141,16 @@ def test_get_natural_key():
|
|||
# THEN: We get a properly sorted list
|
||||
assert sorted_list == ['1st item', 'item 3b', 'item 10a'], 'Numbers should be sorted naturally'
|
||||
|
||||
# GIVEN: The language is still English (a language, which sorts digits before letters)
|
||||
mocked_get_language.return_value = 'en'
|
||||
unsorted_list = ['1 songname', '100 songname', '2 songname']
|
||||
|
||||
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
|
||||
sorted_list = sorted(unsorted_list, key=get_natural_key)
|
||||
|
||||
# THEN: We get a properly sorted list
|
||||
assert sorted_list == ['1 songname', '2 songname', '100 songname'], 'Numbers should be sorted naturally'
|
||||
|
||||
|
||||
def test_check_same_instance():
|
||||
"""
|
||||
|
|
|
@ -25,7 +25,7 @@ import pytest
|
|||
from pathlib import Path
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from openlp.core.common import settings
|
||||
from openlp.core.common import settings as modsettings
|
||||
from openlp.core.common.settings import Settings, media_players_conv, upgrade_dark_theme_to_ui_theme
|
||||
from openlp.core.ui.style import UiThemes
|
||||
|
||||
|
@ -185,7 +185,7 @@ def test_upgrade_single_setting(mocked_remove, mocked_setValue, mocked_value, mo
|
|||
local_settings.__setting_upgrade_99__ = [
|
||||
('single/value', 'single/new value', [(str, '')])
|
||||
]
|
||||
settings.__version__ = 99
|
||||
modsettings.__version__ = 99
|
||||
mocked_value.side_effect = [98, 10]
|
||||
mocked_contains.return_value = True
|
||||
|
||||
|
@ -212,7 +212,7 @@ def test_upgrade_setting_value(mocked_remove, mocked_setValue, mocked_value, moc
|
|||
local_settings.__setting_upgrade_99__ = [
|
||||
('values/old value', 'values/new value', [(True, 1)])
|
||||
]
|
||||
settings.__version__ = 99
|
||||
modsettings.__version__ = 99
|
||||
mocked_value.side_effect = [98, 1]
|
||||
mocked_contains.return_value = True
|
||||
|
||||
|
@ -239,7 +239,7 @@ def test_upgrade_multiple_one_invalid(mocked_remove, mocked_setValue, mocked_val
|
|||
local_settings.__setting_upgrade_99__ = [
|
||||
(['multiple/value 1', 'multiple/value 2'], 'single/new value', [])
|
||||
]
|
||||
settings.__version__ = 99
|
||||
modsettings.__version__ = 99
|
||||
mocked_value.side_effect = [98, 10]
|
||||
mocked_contains.side_effect = [True, False]
|
||||
|
||||
|
@ -296,6 +296,16 @@ def test_convert_value_setting_none_list():
|
|||
assert result == [], 'The result should be an empty list'
|
||||
|
||||
|
||||
def test_convert_value_setting_utf8_json():
|
||||
"""Test the Settings._convert_value() method when a setting is utf8-encoded JSON"""
|
||||
# GIVEN: A settings object
|
||||
# WHEN: _convert_value() is run
|
||||
result = Settings()._convert_value('{"key": "value"}'.encode('utf8'), None)
|
||||
|
||||
# THEN: The result should be a Path object
|
||||
assert isinstance(result, dict), 'The result should be a dictionary'
|
||||
|
||||
|
||||
def test_convert_value_setting_json_Path():
|
||||
"""Test the Settings._convert_value() method when a setting is JSON and represents a Path object"""
|
||||
# GIVEN: A settings object
|
||||
|
|
|
@ -307,7 +307,7 @@ def test_set_startup_screen_hide(display_window_env, mock_settings):
|
|||
'Display.setStartupSplashScreen("orange", "");')
|
||||
|
||||
|
||||
def test_after_loaded(display_window_env, mock_settings):
|
||||
def test_after_loaded(display_window_env, mock_settings, registry):
|
||||
"""
|
||||
Test the correct steps are taken when the webview is loaded
|
||||
"""
|
||||
|
@ -335,7 +335,7 @@ def test_after_loaded(display_window_env, mock_settings):
|
|||
display_window.set_startup_screen.assert_called_once()
|
||||
|
||||
|
||||
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
|
||||
def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings, registry):
|
||||
"""
|
||||
Test the mouse is showing even if the `hide mouse` setting is set while is_display=false
|
||||
"""
|
||||
|
@ -361,7 +361,7 @@ def test_after_loaded_hide_mouse_not_display(display_window_env, mock_settings):
|
|||
'});')
|
||||
|
||||
|
||||
def test_after_loaded_callback(display_window_env, mock_settings):
|
||||
def test_after_loaded_callback(display_window_env, mock_settings, registry):
|
||||
"""
|
||||
Test if the __ is loaded on after_loaded() method correctly
|
||||
"""
|
||||
|
|
|
@ -104,7 +104,7 @@ def test_get_upgrade_op():
|
|||
MockedOperations.assert_called_with(mocked_context)
|
||||
|
||||
|
||||
def test_delete_database_without_db_file_name():
|
||||
def test_delete_database_without_db_file_name(registry):
|
||||
"""
|
||||
Test that the ``delete_database`` function removes a database file, without the file name parameter
|
||||
"""
|
||||
|
|
|
@ -831,3 +831,119 @@ def test_update_recent_files_menu(mocked_create_action, mocked_add_actions, Mock
|
|||
|
||||
# THEN: There should be no errors
|
||||
assert mocked_create_action.call_count == 2
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QProgressDialog')
|
||||
def test_show_wait_dialog(MockProcessDialog, main_window_reduced):
|
||||
"""Test that the show wait dialog context manager works correctly"""
|
||||
# GIVEN: A mocked out QProgressDialog and a minimal main window
|
||||
mocked_wait_dialog = MagicMock()
|
||||
MockProcessDialog.return_value = mocked_wait_dialog
|
||||
|
||||
# WHEN: Calling _show_wait_dialog()
|
||||
with main_window_reduced._show_wait_dialog('Test', 'This is a test'):
|
||||
pass
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
MockProcessDialog.assert_called_once_with('This is a test', '', 0, 0, main_window_reduced)
|
||||
mocked_wait_dialog.setWindowTitle.assert_called_once_with('Test')
|
||||
mocked_wait_dialog.setWindowFlag.assert_called_once_with(QtCore.Qt.WindowContextHelpButtonHint, False)
|
||||
mocked_wait_dialog.setWindowModality.assert_called_once_with(QtCore.Qt.WindowModal)
|
||||
mocked_wait_dialog.setAutoClose.assert_called_once_with(False)
|
||||
mocked_wait_dialog.setCancelButton.assert_called_once_with(None)
|
||||
mocked_wait_dialog.show.assert_called_once_with()
|
||||
mocked_wait_dialog.close.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
|
||||
def test_wait_for_threads(MockApp, main_window_reduced):
|
||||
"""Test that the wait_for_threads() method correctly stops the threads"""
|
||||
# GIVEN: A mocked application, and a reduced main window
|
||||
mocked_http_thread = MagicMock()
|
||||
mocked_http_thread.isRunning.side_effect = [True, True, False, False]
|
||||
mocked_http_worker = MagicMock()
|
||||
mocked_http_worker.stop = MagicMock()
|
||||
main_window_reduced.application.worker_threads = {
|
||||
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
|
||||
}
|
||||
|
||||
# WHEN: _wait_for_threads() is called
|
||||
main_window_reduced._wait_for_threads()
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
assert MockApp.processEvents.call_count == 2, 'processEvents() should have been called twice'
|
||||
mocked_http_worker.stop.assert_called_once()
|
||||
assert mocked_http_thread.isRunning.call_count == 4, 'isRunning() should have been called 4 times'
|
||||
mocked_http_thread.wait.assert_called_once_with(100)
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
|
||||
def test_wait_for_threads_no_threads(MockApp, main_window_reduced):
|
||||
"""Test that the wait_for_threads() method exits early when there are no threads"""
|
||||
# GIVEN: A mocked application, and a reduced main window
|
||||
main_window_reduced.application.worker_threads = {}
|
||||
|
||||
# WHEN: _wait_for_threads() is called
|
||||
main_window_reduced._wait_for_threads()
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
|
||||
def test_wait_for_threads_disappearing_thread(MockApp, main_window_reduced):
|
||||
"""Test that the wait_for_threads() method correctly ignores threads that resolve themselves"""
|
||||
# GIVEN: A mocked application, and a reduced main window
|
||||
main_window_reduced.application.worker_threads = MagicMock(**{'keys.side_effect': [['http'], []]})
|
||||
|
||||
# WHEN: _wait_for_threads() is called
|
||||
main_window_reduced._wait_for_threads()
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
|
||||
def test_wait_for_threads_stuck_thread(MockApp, main_window_reduced):
|
||||
"""Test that the wait_for_threads() method correctly stops the threads"""
|
||||
# GIVEN: A mocked application, and a reduced main window
|
||||
mocked_http_thread = MagicMock()
|
||||
mocked_http_thread.isRunning.return_value = True
|
||||
mocked_http_worker = MagicMock()
|
||||
mocked_http_worker.stop = MagicMock()
|
||||
main_window_reduced.application.worker_threads = {
|
||||
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
|
||||
}
|
||||
|
||||
# WHEN: _wait_for_threads() is called
|
||||
main_window_reduced._wait_for_threads()
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
assert MockApp.processEvents.call_count == 51, 'processEvents() should have been called 51 times'
|
||||
mocked_http_worker.stop.assert_called_once()
|
||||
assert mocked_http_thread.isRunning.call_count == 53, 'isRunning() should have been called 53 times'
|
||||
mocked_http_thread.wait.assert_called_with(100)
|
||||
mocked_http_thread.terminate.assert_called_once()
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
|
||||
def test_wait_for_threads_runtime_error(MockApp, main_window_reduced):
|
||||
"""Test that the wait_for_threads() method handles a runtime error"""
|
||||
# GIVEN: A mocked application, and a reduced main window
|
||||
mocked_http_thread = MagicMock()
|
||||
mocked_http_thread.isRunning.side_effect = RuntimeError
|
||||
mocked_http_worker = MagicMock()
|
||||
mocked_http_worker.stop = MagicMock()
|
||||
main_window_reduced.application.worker_threads = {
|
||||
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
|
||||
}
|
||||
|
||||
# WHEN: _wait_for_threads() is called
|
||||
main_window_reduced._wait_for_threads()
|
||||
|
||||
# THEN: The correct methods should have been called
|
||||
assert MockApp.processEvents.call_count == 1, 'processEvents() should have been called once'
|
||||
mocked_http_worker.stop.assert_called_once()
|
||||
assert mocked_http_thread.isRunning.call_count == 1, 'isRunning() should have been called once'
|
||||
mocked_http_thread.wait.assert_not_called()
|
||||
mocked_http_thread.terminate.assert_not_called()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,179 +22,194 @@
|
|||
Package to test the openlp.plugin.bible.lib.https package.
|
||||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.bibles.lib.importers.http import BGExtract, BSExtract, CWExtract
|
||||
|
||||
IS_CI = 'GITLAB_CI' in os.environ or 'APPVEYOR' in os.environ
|
||||
|
||||
if 'GITLAB_CI' in os.environ or 'APPVEYOR' in os.environ:
|
||||
pytest.skip('Skip Bible HTTP tests to prevent GitLab CI from being blacklisted', allow_module_level=True)
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_CI, reason='Skip Bible HTTP tests to prevent GitLab CI from being blacklisted')
|
||||
class TestBibleHTTP(TestCase):
|
||||
@pytest.fixture
|
||||
def bg_extract(settings, registry):
|
||||
"""A fixture to return a BibleGateway extractor"""
|
||||
registry.register('service_list', MagicMock())
|
||||
registry.register('main_window', MagicMock())
|
||||
bg = BGExtract()
|
||||
yield bg
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the Registry
|
||||
"""
|
||||
Registry.create()
|
||||
Registry().register('service_list', MagicMock())
|
||||
Registry().register('application', MagicMock())
|
||||
Registry().register('main_window', MagicMock())
|
||||
Registry().register('settings', Settings())
|
||||
|
||||
def test_bible_gateway_extract_books(self):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of book list for NIV bible
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = BGExtract()
|
||||
@pytest.fixture
|
||||
def cw_extract(settings, registry):
|
||||
"""A fixture to return a Crosswalk extractor"""
|
||||
registry.register('service_list', MagicMock())
|
||||
registry.register('main_window', MagicMock())
|
||||
cw = CWExtract()
|
||||
yield cw
|
||||
|
||||
# WHEN: The Books list is called
|
||||
books = handler.get_books_from_http('NIV')
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'The bible should not have had any books added or removed'
|
||||
assert books[0] == 'Genesis', 'The first bible book should be Genesis'
|
||||
@pytest.fixture
|
||||
def bs_extract(settings, registry):
|
||||
"""A fixture to return a BibleServer extractor"""
|
||||
registry.register('service_list', MagicMock())
|
||||
registry.register('main_window', MagicMock())
|
||||
bs = BSExtract()
|
||||
yield bs
|
||||
|
||||
def test_bible_gateway_extract_books_support_redirect(self):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of book list for DN1933 bible with redirect (bug 1251437)
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = BGExtract()
|
||||
|
||||
# WHEN: The Books list is called
|
||||
books = handler.get_books_from_http('DN1933')
|
||||
def test_biblegateway_get_bibles(bg_extract):
|
||||
"""
|
||||
Test getting list of bibles from BibleGateway.com
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: downloading bible list from Crosswalk
|
||||
bibles = bg_extract.get_bibles_from_http()
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'This bible should have 66 books'
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('Holman Christian Standard Bible (HCSB)', 'HCSB', 'en') in bibles
|
||||
|
||||
def test_bible_gateway_extract_verse(self):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of verse list for NIV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = BGExtract()
|
||||
|
||||
# WHEN: The Books list is called
|
||||
results = handler.get_bible_chapter('NIV', 'John', 3)
|
||||
def test_bible_gateway_extract_books(bg_extract):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of book list for NIV bible
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
books = bg_extract.get_books_from_http('NIV')
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'The bible should not have had any books added or removed'
|
||||
assert books[0] == 'Genesis', 'The first bible book should be Genesis'
|
||||
|
||||
def test_bible_gateway_extract_verse_nkjv(self):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of verse list for NKJV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = BGExtract()
|
||||
|
||||
# WHEN: The Books list is called
|
||||
results = handler.get_bible_chapter('NKJV', 'John', 3)
|
||||
def test_bible_gateway_extract_books_support_redirect(bg_extract):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of book list for DN1933 bible with redirect (bug 1251437)
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
books = bg_extract.get_books_from_http('DN1933')
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'This bible should have 66 books'
|
||||
|
||||
def test_crosswalk_extract_books(self):
|
||||
"""
|
||||
Test Crosswalk retrieval of book list for NIV bible
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = CWExtract()
|
||||
|
||||
# WHEN: The Books list is called
|
||||
books = handler.get_books_from_http('niv')
|
||||
def test_bible_gateway_extract_verse(bg_extract):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of verse list for NIV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
results = bg_extract.get_bible_chapter('NIV', 'John', 3)
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'The bible should not have had any books added or removed'
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
|
||||
def test_crosswalk_extract_verse(self):
|
||||
"""
|
||||
Test Crosswalk retrieval of verse list for NIV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = CWExtract()
|
||||
|
||||
# WHEN: The Books list is called
|
||||
results = handler.get_bible_chapter('niv', 'john', 3)
|
||||
def test_bible_gateway_extract_verse_nkjv(bg_extract):
|
||||
"""
|
||||
Test the Bible Gateway retrieval of verse list for NKJV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
results = bg_extract.get_bible_chapter('NKJV', 'John', 3)
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
|
||||
def test_crosswalk_get_bibles(self):
|
||||
"""
|
||||
Test getting list of bibles from Crosswalk.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
handler = CWExtract()
|
||||
|
||||
# WHEN: downloading bible list from Crosswalk
|
||||
bibles = handler.get_bibles_from_http()
|
||||
def test_crosswalk_extract_books(cw_extract):
|
||||
"""
|
||||
Test Crosswalk retrieval of book list for NIV bible
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
books = cw_extract.get_books_from_http('niv')
|
||||
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('Giovanni Diodati 1649 (Italian)', 'gdb', 'it') in bibles
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(books) == 66, 'The bible should not have had any books added or removed'
|
||||
|
||||
def test_crosswalk_get_verse_text(self):
|
||||
"""
|
||||
Test verse text from Crosswalk.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
handler = CWExtract()
|
||||
|
||||
# WHEN: downloading NIV Genesis from Crosswalk
|
||||
niv_genesis_chapter_one = handler.get_bible_chapter('niv', 'Genesis', 1)
|
||||
def test_crosswalk_extract_verse(cw_extract):
|
||||
"""
|
||||
Test Crosswalk retrieval of verse list for NIV bible John 3
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
# WHEN: The Books list is called
|
||||
results = cw_extract.get_bible_chapter('niv', 'john', 3)
|
||||
|
||||
# THEN: The verse list should contain the verses
|
||||
assert niv_genesis_chapter_one.has_verse_list() is True
|
||||
assert 'In the beginning God created the heavens and the earth.' == niv_genesis_chapter_one.verse_list[1], \
|
||||
'The first chapter of genesis should have been fetched.'
|
||||
# THEN: We should get back a valid service item
|
||||
assert len(results.verse_list) == 36, 'The book of John should not have had any verses added or removed'
|
||||
|
||||
def test_bibleserver_get_bibles(self):
|
||||
"""
|
||||
Test getting list of bibles from BibleServer.com
|
||||
"""
|
||||
# GIVEN: A new Bible Server extraction class
|
||||
handler = BSExtract()
|
||||
|
||||
# WHEN: downloading bible list from bibleserver
|
||||
bibles = handler.get_bibles_from_http()
|
||||
def test_crosswalk_get_bibles(cw_extract):
|
||||
"""
|
||||
Test getting list of bibles from Crosswalk.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
# WHEN: downloading bible list from Crosswalk
|
||||
bibles = cw_extract.get_bibles_from_http()
|
||||
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('New Int. Readers Version', 'NIRV', 'en') in bibles
|
||||
assert ('Священное Писание, Восточный перевод', 'CARS', 'ru') in bibles
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('Giovanni Diodati 1649 (Italian)', 'gdb', 'it') in bibles
|
||||
|
||||
def test_bibleserver_get_verse_text(self):
|
||||
"""
|
||||
Test verse text from bibleserver.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
handler = BSExtract()
|
||||
|
||||
# WHEN: downloading NIV Genesis from Crosswalk
|
||||
niv_genesis_chapter_one = handler.get_bible_chapter('NIV', 'Genesis', 1)
|
||||
def test_crosswalk_get_verse_text(cw_extract):
|
||||
"""
|
||||
Test verse text from Crosswalk.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
# WHEN: downloading NIV Genesis from Crosswalk
|
||||
niv_genesis_chapter_one = cw_extract.get_bible_chapter('niv', 'Genesis', 1)
|
||||
|
||||
# THEN: The verse list should contain the verses
|
||||
assert niv_genesis_chapter_one.has_verse_list() is True
|
||||
assert 'In the beginning God created the heavens and the earth.' == niv_genesis_chapter_one.verse_list[1], \
|
||||
'The first chapter of genesis should have been fetched.'
|
||||
# THEN: The verse list should contain the verses
|
||||
assert niv_genesis_chapter_one.has_verse_list() is True
|
||||
assert 'In the beginning God created the heavens and the earth.' == niv_genesis_chapter_one.verse_list[1], \
|
||||
'The first chapter of genesis should have been fetched.'
|
||||
|
||||
def test_biblegateway_get_bibles(self):
|
||||
"""
|
||||
Test getting list of bibles from BibleGateway.com
|
||||
"""
|
||||
# GIVEN: A new Bible Gateway extraction class
|
||||
handler = BGExtract()
|
||||
|
||||
# WHEN: downloading bible list from Crosswalk
|
||||
bibles = handler.get_bibles_from_http()
|
||||
def test_bibleserver_get_bibles(bs_extract):
|
||||
"""
|
||||
Test getting list of bibles from BibleServer.com
|
||||
"""
|
||||
# GIVEN: A new Bible Server extraction class
|
||||
# WHEN: downloading bible list from bibleserver
|
||||
bibles = bs_extract.get_bibles_from_http()
|
||||
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('Holman Christian Standard Bible (HCSB)', 'HCSB', 'en') in bibles
|
||||
# THEN: The list should not be None, and some known bibles should be there
|
||||
assert bibles is not None
|
||||
assert ('New Int. Readers Version', 'NIRV', 'en') in bibles
|
||||
assert ('Священное Писание, Восточный перевод', 'CARS', 'ru') in bibles
|
||||
|
||||
|
||||
def test_bibleserver_get_verse_text(bs_extract):
|
||||
"""
|
||||
Test verse text from bibleserver.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
# WHEN: downloading NIV Genesis from Crosswalk
|
||||
niv_genesis_chapter_one = bs_extract.get_bible_chapter('NIV', 'Genesis', 1)
|
||||
|
||||
# THEN: The verse list should contain the verses
|
||||
assert niv_genesis_chapter_one.has_verse_list() is True
|
||||
assert 'In the beginning God created the heavens and the earth.' == niv_genesis_chapter_one.verse_list[1], \
|
||||
'The first chapter of genesis should have been fetched.'
|
||||
|
||||
|
||||
def test_bibleserver_get_chapter_with_bridged_verses(bs_extract):
|
||||
"""
|
||||
Test verse text from bibleserver.com
|
||||
"""
|
||||
# GIVEN: A new Crosswalk extraction class
|
||||
# WHEN: downloading PCB Genesis from BibleServer
|
||||
pcb_genesis_chapter_one = bs_extract.get_bible_chapter('PCB', 'Genesis', 1)
|
||||
|
||||
# THEN: The verse list should contain the verses
|
||||
assert pcb_genesis_chapter_one.has_verse_list() is True
|
||||
assert 7 in pcb_genesis_chapter_one.verse_list
|
||||
assert 8 not in pcb_genesis_chapter_one.verse_list
|
||||
|
|
|
@ -201,3 +201,25 @@ class TestOpenLyricsImport(TestCase, TestMixin):
|
|||
# THEN: The song should preserve spaces before chords
|
||||
import_content = importer.open_lyrics.xml_to_song.call_args[0][0]
|
||||
assert import_content == expected_content
|
||||
|
||||
def test_lines_spacing_is_correctly_trimmed(self):
|
||||
"""
|
||||
Test if lines' leading space are trimmed correctly
|
||||
"""
|
||||
# GIVEN: One OpenLyrics XML with the <lines> tag (Amazing_Grace_4_chords.xml)
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OpenLyricsImport(mocked_manager, file_paths=[])
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
expected_content_file = TEST_PATH / 'Amazing_Grace_4_chords_result.xml'
|
||||
expected_content = expected_content_file.read_text()
|
||||
|
||||
# WHEN: Importing the file not having those whitespaces...
|
||||
importer.import_source = [TEST_PATH / 'Amazing_Grace_4_chords.xml']
|
||||
importer.open_lyrics = MagicMock()
|
||||
importer.open_lyrics.xml_to_song = MagicMock()
|
||||
importer.do_import()
|
||||
|
||||
# THEN: The song should be correctly trimmed on start and end
|
||||
import_content = importer.open_lyrics.xml_to_song.call_args[0][0]
|
||||
assert import_content == expected_content
|
||||
|
|
|
@ -52,10 +52,11 @@ if CAN_RUN_TESTS:
|
|||
"""
|
||||
This class logs changes in the title instance variable
|
||||
"""
|
||||
_title_assignment_list = []
|
||||
_title_assignment_list = None
|
||||
|
||||
def __init__(self, manager):
|
||||
WorshipCenterProImport.__init__(self, manager, file_paths=[])
|
||||
self._title_assignment_list = []
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
|
@ -63,7 +64,10 @@ if CAN_RUN_TESTS:
|
|||
|
||||
@title.setter
|
||||
def title(self, title):
|
||||
self._title_assignment_list.append(title)
|
||||
try:
|
||||
self._title_assignment_list.append(title)
|
||||
except AttributeError:
|
||||
self._title_assignment_list = [title]
|
||||
|
||||
|
||||
RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
|
||||
|
@ -71,21 +75,27 @@ RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
|
|||
DBTestRecord(1, 'CCLISONGID', '12345'),
|
||||
DBTestRecord(1, 'COMMENTS', 'The original version'),
|
||||
DBTestRecord(1, 'COPY', 'Public Domain'),
|
||||
DBTestRecord(1, 'SUBJECT', 'Grace'),
|
||||
DBTestRecord(
|
||||
1, 'LYRICS',
|
||||
'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
|
||||
'<INTRO>Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
|
||||
'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;'
|
||||
'\'Twas grace that&crlf;taught my heart to fear,&crlf;And grace my fears relieved;&crlf;'
|
||||
'<PRECHORUS>\'Twas grace that&crlf;taught my heart to fear,&crlf;'
|
||||
'And grace my fears relieved;&crlf;'
|
||||
'How precious did&crlf;that grace appear&crlf;The hour I first believed.&crlf;&crlf;'
|
||||
'Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;'
|
||||
'<CHORUS>Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;'
|
||||
'\'Tis grace hath brought&crlf;me safe thus far,&crlf;'
|
||||
'And grace will lead me home.&crlf;&crlf;The Lord has&crlf;promised good to me,&crlf;'
|
||||
'And grace will lead me home.&crlf;&crlf;'
|
||||
'<REFRAIN>The Lord has&crlf;promised good to me,&crlf;'
|
||||
'His Word my hope secures;&crlf;He will my Shield&crlf;and Portion be,&crlf;'
|
||||
'As long as life endures.&crlf;&crlf;Yea, when this flesh&crlf;and heart shall fail,&crlf;'
|
||||
'As long as life endures.&crlf;&crlf;'
|
||||
'<BRIDGE>Yea, when this flesh&crlf;and heart shall fail,&crlf;'
|
||||
'And mortal life shall cease,&crlf;I shall possess,&crlf;within the veil,&crlf;'
|
||||
'A life of joy and peace.&crlf;&crlf;The earth shall soon&crlf;dissolve like snow,&crlf;'
|
||||
'A life of joy and peace.&crlf;&crlf;'
|
||||
'<TAG>The earth shall soon&crlf;dissolve like snow,&crlf;'
|
||||
'The sun forbear to shine;&crlf;But God, Who called&crlf;me here below,&crlf;'
|
||||
'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;'
|
||||
'Shall be forever mine.&crlf;&crlf;'
|
||||
'<END>When we\'ve been there&crlf;ten thousand years,&crlf;'
|
||||
'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;'
|
||||
'Than when we\'d first begun.&crlf;&crlf;'),
|
||||
DBTestRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'),
|
||||
|
@ -105,32 +115,32 @@ RECORDSET_TEST_DATA = [DBTestRecord(1, 'TITLE', 'Amazing Grace'),
|
|||
SONG_TEST_DATA = [{'title': 'Amazing Grace',
|
||||
'verses': [
|
||||
('Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n'
|
||||
'but now am found;\nWas blind, but now I see.'),
|
||||
'but now am found;\nWas blind, but now I see.', 'i'),
|
||||
('\'Twas grace that\ntaught my heart to fear,\nAnd grace my fears relieved;\nHow precious did\n'
|
||||
'that grace appear\nThe hour I first believed.'),
|
||||
'that grace appear\nThe hour I first believed.', 'p'),
|
||||
('Through many dangers,\ntoils and snares,\nI have already come;\n\'Tis grace hath brought\n'
|
||||
'me safe thus far,\nAnd grace will lead me home.'),
|
||||
'me safe thus far,\nAnd grace will lead me home.', 'c'),
|
||||
('The Lord has\npromised good to me,\nHis Word my hope secures;\n'
|
||||
'He will my Shield\nand Portion be,\nAs long as life endures.'),
|
||||
'He will my Shield\nand Portion be,\nAs long as life endures.', 'c'),
|
||||
('Yea, when this flesh\nand heart shall fail,\nAnd mortal life shall cease,\nI shall possess,\n'
|
||||
'within the veil,\nA life of joy and peace.'),
|
||||
'within the veil,\nA life of joy and peace.', 'b'),
|
||||
('The earth shall soon\ndissolve like snow,\nThe sun forbear to shine;\nBut God, Who called\n'
|
||||
'me here below,\nShall be forever mine.'),
|
||||
'me here below,\nShall be forever mine.', 'o'),
|
||||
('When we\'ve been there\nten thousand years,\nBright shining as the sun,\n'
|
||||
'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.')],
|
||||
'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.', 'e')],
|
||||
'author': 'John Newton',
|
||||
'comments': 'The original version',
|
||||
'copyright': 'Public Domain'},
|
||||
{'title': 'Beautiful Garden Of Prayer, The',
|
||||
'verses': [
|
||||
('There\'s a garden where\nJesus is waiting,\nThere\'s a place that\nis wondrously fair,\n'
|
||||
'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.'),
|
||||
'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.', 'v'),
|
||||
('Oh, the beautiful garden,\nthe garden of prayer!\nOh, the beautiful\ngarden of prayer!\n'
|
||||
'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.'),
|
||||
'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.', 'v'),
|
||||
('There\'s a garden where\nJesus is waiting,\nAnd I go with my\nburden and care,\n'
|
||||
'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.'),
|
||||
'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.', 'v'),
|
||||
('There\'s a garden where\nJesus is waiting,\nAnd He bids you to come,\nmeet Him there;\n'
|
||||
'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.')]}]
|
||||
'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.', 'v')]}]
|
||||
|
||||
|
||||
@skipUnless(CAN_RUN_TESTS, 'Not Windows, skipping test')
|
||||
|
@ -233,11 +243,44 @@ class TestWorshipCenterProSongImport(TestCase):
|
|||
verse_calls = song_data['verses']
|
||||
add_verse_call_count += len(verse_calls)
|
||||
for call in verse_calls:
|
||||
mocked_add_verse.assert_any_call(call, 'v')
|
||||
mocked_add_verse.assert_any_call(*call)
|
||||
if 'author' in song_data:
|
||||
mocked_parse_author.assert_any_call(song_data['author'])
|
||||
if 'comments' in song_data:
|
||||
mocked_add_comment.assert_any_call(song_data['comments'])
|
||||
if 'copyright' in song_data:
|
||||
mocked_add_copyright.assert_any_call(song_data['copyright'])
|
||||
if 'subject' in song_data:
|
||||
mocked_add_copyright.assert_any_call(song_data['subject'])
|
||||
assert mocked_add_verse.call_count == add_verse_call_count, 'Incorrect number of calls made to add_verse'
|
||||
|
||||
def test_song_import_stop(self):
|
||||
"""Test that the song importer stops when the flag is set"""
|
||||
with patch('openlp.plugins.songs.lib.importers.worshipcenterpro.SongImport'), \
|
||||
patch('openlp.plugins.songs.lib.importers.worshipcenterpro.pyodbc') as mocked_pyodbc, \
|
||||
patch('openlp.plugins.songs.lib.importers.worshipcenterpro.translate') as mocked_translate:
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
mocked_add_verse = MagicMock()
|
||||
mocked_parse_author = MagicMock()
|
||||
mocked_add_comment = MagicMock()
|
||||
mocked_add_copyright = MagicMock()
|
||||
mocked_finish = MagicMock()
|
||||
mocked_pyodbc.connect().cursor().fetchall.return_value = RECORDSET_TEST_DATA
|
||||
mocked_translate.return_value = 'Translated Text'
|
||||
importer = WorshipCenterProImportLogger(mocked_manager)
|
||||
importer.import_source = 'import_source'
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.add_verse = mocked_add_verse
|
||||
importer.parse_author = mocked_parse_author
|
||||
importer.add_comment = mocked_add_comment
|
||||
importer.add_copyright = mocked_add_copyright
|
||||
importer.stop_import_flag = True
|
||||
importer.finish = mocked_finish
|
||||
|
||||
# WHEN: Calling the do_import method
|
||||
importer.do_import()
|
||||
|
||||
# THEN: No songs should have been imported
|
||||
assert len(importer._title_assignment_list) == 0
|
||||
mocked_finish.assert_not_called()
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<song xmlns="http://openlyrics.info/namespace/2009/song"
|
||||
version="0.8"
|
||||
createdIn="OpenLP 2.9.5"
|
||||
modifiedIn="MyApp 0.0.1"
|
||||
modifiedDate="2022-12-12T00:01:00+10:00">
|
||||
<properties>
|
||||
<titles>
|
||||
<title>Amazing Grace</title>
|
||||
</titles>
|
||||
</properties>
|
||||
<lyrics>
|
||||
<verse name="v1">
|
||||
<lines>
|
||||
The <chord name="C"/>Amazing <chord name="C7"/>grace, how s<chord name="F"/>weet the s<chord name="Fm"/>ound<br/>That <chord name="C"/>saved a <chord name="Am7"/>wretch like<chord name="D"/> me!
|
||||
</lines>
|
||||
</verse>
|
||||
</lyrics>
|
||||
</song>
|
|
@ -0,0 +1 @@
|
|||
<song xmlns="http://openlyrics.info/namespace/2009/song" version="0.8" createdIn="OpenLP 2.9.5" modifiedIn="MyApp 0.0.1" modifiedDate="2022-12-12T00:01:00+10:00"><properties><titles><title>Amazing Grace</title></titles></properties><lyrics><verse name="v1"><lines>The <chord name="C"/>Amazing <chord name="C7"/>grace, how s<chord name="F"/>weet the s<chord name="Fm"/>ound<br/>That <chord name="C"/>saved a <chord name="Am7"/>wretch like<chord name="D"/> me!</lines></verse></lyrics></song>
|
Loading…
Reference in New Issue