Merge branch 'master' into list-view

This commit is contained in:
Mateus Meyer Jiacomelli 2023-02-10 22:19:41 -03:00
commit a22284fd0c
68 changed files with 37348 additions and 24126 deletions

View File

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

View File

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

View File

@ -1 +1 @@
3.0.0.dev6+dbea8bf56
3.0.2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

12101
resources/i18n/it_CH.ts Normal file

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

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

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

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

View File

@ -118,7 +118,7 @@ using a computer and a display/projector.""",
'QtAwesome',
"qrcode",
'requests',
'SQLAlchemy >= 0.5',
'SQLAlchemy < 1.5',
'waitress',
'WebOb',
'websockets',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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