mirror of https://gitlab.com/openlp/openlp.git
Merge branch 'master' into remote-sync
This commit is contained in:
commit
e1b3e3d9a9
|
@ -31,7 +31,7 @@ install:
|
|||
# Update pip
|
||||
- python -m pip install --upgrade pip
|
||||
# Install generic dependencies from pypi.
|
||||
- python -m pip install sqlalchemy alembic platformdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc flask-cors pytest-qt pyenchant pysword qrcode flask
|
||||
- python -m pip install sqlalchemy alembic platformdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc flask-cors pytest-qt pyenchant pysword qrcode flask packaging
|
||||
# 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.1.0
|
||||
3.1.1
|
||||
|
|
|
@ -24,6 +24,7 @@ from flask import jsonify, request, abort, Blueprint
|
|||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.api.lib import login_required
|
||||
from openlp.core.common.i18n import LanguageManager
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib.plugin import PluginStatus, StringContent
|
||||
from openlp.core.state import State
|
||||
|
@ -56,16 +57,38 @@ def plugin_list():
|
|||
return jsonify(searches)
|
||||
|
||||
|
||||
@core.route('/shortcuts')
|
||||
def shortcuts():
|
||||
data = []
|
||||
settings = Registry().get('settings_thread')
|
||||
shortcut_prefix = 'shortcuts/'
|
||||
for key in settings.allKeys():
|
||||
if key.startswith(shortcut_prefix):
|
||||
data.append(
|
||||
{
|
||||
'action': key.removeprefix(shortcut_prefix),
|
||||
'shortcut': settings.value(key)
|
||||
}
|
||||
)
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@core.route('/system')
|
||||
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'] = 4
|
||||
data['api_revision'] = 5
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@core.route('/language')
|
||||
def language():
|
||||
language = LanguageManager.get_language()
|
||||
return jsonify({'language': language})
|
||||
|
||||
|
||||
@core.route('/login', methods=['POST'])
|
||||
def login():
|
||||
data = request.json
|
||||
|
|
|
@ -485,12 +485,12 @@ def main():
|
|||
# support dark mode on windows 10. This makes the titlebar dark, the rest is setup later
|
||||
# by calling set_windows_darkmode
|
||||
qt_args.extend(['-platform', 'windows:darkmode=1'])
|
||||
elif is_macosx() and getattr(sys, 'frozen', False) and not os.environ.get('QTWEBENGINEPROCESS_PATH'):
|
||||
# Work around an issue where PyInstaller is not setting this environment variable
|
||||
os.environ['QTWEBENGINEPROCESS_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / 'PyQt5' / 'Qt5' /
|
||||
'lib' / 'QtWebEngineCore.framework' / 'Versions' / '5' /
|
||||
'Helpers' / 'QtWebEngineProcess.app' / 'Contents' / 'MacOS' /
|
||||
'QtWebEngineProcess')
|
||||
elif is_macosx() and getattr(sys, 'frozen', False):
|
||||
# Set the location to the QtWebEngineProcess binary, normally set by PyInstaller, but it moves around...
|
||||
os.environ['QTWEBENGINEPROCESS_PATH'] = str((AppLocation.get_directory(AppLocation.AppDir) / '..' /
|
||||
'Frameworks' / 'QtWebEngineCore.framework' / 'Versions' / '5' /
|
||||
'Helpers' / 'QtWebEngineProcess.app' / 'Contents' / 'MacOS' /
|
||||
'QtWebEngineProcess').resolve())
|
||||
no_custom_factor_rounding = not ('QT_SCALE_FACTOR_ROUNDING_POLICY' in os.environ
|
||||
and bool(os.environ['QT_SCALE_FACTOR_ROUNDING_POLICY'].strip()))
|
||||
if no_custom_factor_rounding:
|
||||
|
@ -502,9 +502,11 @@ def main():
|
|||
app = OpenLP()
|
||||
Registry.create()
|
||||
QtWidgets.QApplication.setOrganizationName('OpenLP')
|
||||
QtWidgets.QApplication.setOrganizationName('openlp.org')
|
||||
QtWidgets.QApplication.setApplicationName('OpenLP')
|
||||
QtWidgets.QApplication.setOrganizationDomain('openlp.org')
|
||||
if args.portable:
|
||||
# This has to be done here so that we can load the settings before instantiating the application object
|
||||
QtWidgets.QApplication.setApplicationName('OpenLPPortable')
|
||||
portable_path, settings = setup_portable_settings(args.portablepath)
|
||||
else:
|
||||
settings = Settings()
|
||||
|
@ -529,7 +531,6 @@ def main():
|
|||
font.setPointSizeF(font.pointSizeF() * application.devicePixelRatio())
|
||||
application.setFont(font)
|
||||
if args.portable:
|
||||
application.setApplicationName('OpenLPPortable')
|
||||
data_path = portable_path / 'Data'
|
||||
set_up_logging(portable_path / 'Other')
|
||||
set_up_web_engine_cache(portable_path / 'Other' / 'web_cache')
|
||||
|
@ -540,7 +541,6 @@ def main():
|
|||
settings.setValue('advanced/is portable', True)
|
||||
settings.sync()
|
||||
else:
|
||||
application.setApplicationName('OpenLP')
|
||||
set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
|
||||
set_up_web_engine_cache(AppLocation.get_directory(AppLocation.CacheDir) / 'web_cache')
|
||||
settings.init_default_shortcuts()
|
||||
|
|
|
@ -27,6 +27,7 @@ import sys
|
|||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
from packaging.version import parse
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
|
@ -114,8 +115,7 @@ class VersionWorker(ThreadWorker):
|
|||
retries += 1
|
||||
else:
|
||||
self.no_internet.emit()
|
||||
if remote_version and (QtCore.QVersionNumber.fromString(remote_version) >
|
||||
QtCore.QVersionNumber.fromString(self.current_version['full'])):
|
||||
if remote_version and (parse(remote_version) > parse(self.current_version['full'])):
|
||||
self.new_version.emit(remote_version)
|
||||
self.quit.emit()
|
||||
|
||||
|
|
|
@ -309,15 +309,15 @@ class Ui_EditSongDialog(object):
|
|||
self.verse_delete_button.setText(UiStrings().Delete)
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.lyrics_tab),
|
||||
translate('SongsPlugin.EditSongForm', 'Title && Lyrics'))
|
||||
self.authors_group_box.setTitle(SongStrings.Authors)
|
||||
self.authors_group_box.setTitle(SongStrings().Authors)
|
||||
self.author_add_button.setText(translate('SongsPlugin.EditSongForm', '&Add to Song'))
|
||||
self.author_edit_button.setText(translate('SongsPlugin.EditSongForm', '&Edit Author Type'))
|
||||
self.author_remove_button.setText(translate('SongsPlugin.EditSongForm', '&Remove'))
|
||||
self.maintenance_button.setText(translate('SongsPlugin.EditSongForm', '&Manage Authors, Topics, Songbooks'))
|
||||
self.topics_group_box.setTitle(SongStrings.Topics)
|
||||
self.topics_group_box.setTitle(SongStrings().Topics)
|
||||
self.topic_add_button.setText(translate('SongsPlugin.EditSongForm', 'A&dd to Song'))
|
||||
self.topic_remove_button.setText(translate('SongsPlugin.EditSongForm', 'R&emove'))
|
||||
self.songbook_group_box.setTitle(SongStrings.SongBooks)
|
||||
self.songbook_group_box.setTitle(SongStrings().SongBooks)
|
||||
self.songbook_add_button.setText(translate('SongsPlugin.EditSongForm', 'Add &to Song'))
|
||||
self.songbook_remove_button.setText(translate('SongsPlugin.EditSongForm', 'Re&move'))
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.authors_tab),
|
||||
|
@ -325,7 +325,7 @@ class Ui_EditSongDialog(object):
|
|||
self.theme_group_box.setTitle(UiStrings().Theme)
|
||||
self.theme_add_button.setText(translate('SongsPlugin.EditSongForm', 'New &Theme'))
|
||||
self.rights_group_box.setTitle(translate('SongsPlugin.EditSongForm', 'Copyright Information'))
|
||||
self.copyright_insert_button.setText(SongStrings.CopyrightSymbol)
|
||||
self.copyright_insert_button.setText(SongStrings().CopyrightSymbol)
|
||||
self.ccli_label.setText(UiStrings().CCLISongNumberLabel)
|
||||
self.comments_group_box.setTitle(translate('SongsPlugin.EditSongForm', 'Comments'))
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.theme_tab),
|
||||
|
|
|
@ -278,6 +278,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
name=VerseType.translated_name(tag[0]),
|
||||
number=tag[1:]))
|
||||
return False
|
||||
if self.audio_list_widget.count() > 1:
|
||||
self.song_tab_widget.setCurrentIndex(3)
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file. Remove items from '
|
||||
'Linked Audio other than the one you wish to keep.'))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_tags(self, tags, first_time=True):
|
||||
|
@ -911,7 +917,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
text = self.copyright_edit.text()
|
||||
pos = self.copyright_edit.cursorPosition()
|
||||
sign = SongStrings.CopyrightSymbol
|
||||
sign = SongStrings().CopyrightSymbol
|
||||
text = text[:pos] + sign + text[pos:]
|
||||
self.copyright_edit.setText(text)
|
||||
self.copyright_edit.setFocus()
|
||||
|
@ -942,6 +948,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
Loads file(s) from the filesystem.
|
||||
"""
|
||||
if self.audio_list_widget.count() > 0:
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file.'))
|
||||
return
|
||||
filters = '{text} (*)'.format(text=UiStrings().AllFiles)
|
||||
file_paths, filter_used = FileDialog.getOpenFileNames(
|
||||
parent=self, caption=translate('SongsPlugin.EditSongForm', 'Open File(s)'), filter=filters)
|
||||
|
@ -954,6 +964,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
Loads file(s) from the media plugin.
|
||||
"""
|
||||
if self.audio_list_widget.count() > 0:
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file.'))
|
||||
return
|
||||
if self.media_form.exec():
|
||||
for file_path in self.media_form.get_selected_files():
|
||||
item = QtWidgets.QListWidgetItem(file_path.name)
|
||||
|
|
|
@ -141,10 +141,10 @@ class Ui_SongMaintenanceDialog(object):
|
|||
"""
|
||||
Translate the UI on the fly.
|
||||
"""
|
||||
song_maintenance_dialog.setWindowTitle(SongStrings.SongMaintenance)
|
||||
self.authors_list_item.setText(SongStrings.Authors)
|
||||
self.topics_list_item.setText(SongStrings.Topics)
|
||||
self.books_list_item.setText(SongStrings.SongBooks)
|
||||
song_maintenance_dialog.setWindowTitle(SongStrings().SongMaintenance)
|
||||
self.authors_list_item.setText(SongStrings().Authors)
|
||||
self.topics_list_item.setText(SongStrings().Topics)
|
||||
self.books_list_item.setText(SongStrings().SongBooks)
|
||||
self.add_author_button.setText(UiStrings().Add)
|
||||
self.edit_author_button.setText(UiStrings().Edit)
|
||||
self.delete_author_button.setText(UiStrings().Delete)
|
||||
|
@ -154,7 +154,7 @@ class Ui_SongMaintenanceDialog(object):
|
|||
self.add_book_button.setText(UiStrings().Add)
|
||||
self.edit_book_button.setText(UiStrings().Edit)
|
||||
self.delete_book_button.setText(UiStrings().Delete)
|
||||
type_list_width = max(self.fontMetrics().width(SongStrings.Authors),
|
||||
self.fontMetrics().width(SongStrings.Topics),
|
||||
self.fontMetrics().width(SongStrings.SongBooks))
|
||||
type_list_width = max(self.fontMetrics().width(SongStrings().Authors),
|
||||
self.fontMetrics().width(SongStrings().Topics),
|
||||
self.fontMetrics().width(SongStrings().SongBooks))
|
||||
self.type_list_widget.setFixedWidth(type_list_width + self.type_list_widget.iconSize().width() + 32)
|
||||
|
|
|
@ -125,7 +125,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
|||
self.song_progress_bar.setValue(2)
|
||||
song_filename = self.current_download_item.downloadDirectory() + '/' \
|
||||
+ self.current_download_item.downloadFileName()
|
||||
song_file = open(song_filename, 'rt')
|
||||
song_file = open(song_filename, 'rt', encoding='utf-8')
|
||||
song_content = song_file.read()
|
||||
song_file.seek(0)
|
||||
if self.check_for_duplicate(song_content):
|
||||
|
|
|
@ -384,7 +384,7 @@ def clean_song(manager, song):
|
|||
song.search_lyrics = ' '.join([clean_string(remove_tags(verse[1], True)) for verse in verses])
|
||||
# The song does not have any author, add one.
|
||||
if not song.authors_songs:
|
||||
name = SongStrings.AuthorUnknown
|
||||
name = SongStrings().AuthorUnknown
|
||||
author = manager.get_object_filtered(Author, Author.display_name == name)
|
||||
if author is None:
|
||||
author = Author(display_name=name, last_name='', first_name='')
|
||||
|
|
|
@ -166,28 +166,29 @@ class SongFormat(object):
|
|||
EasyWorshipDB = 7
|
||||
EasyWorshipSqliteDB = 8
|
||||
EasyWorshipService = 9
|
||||
FoilPresenter = 10
|
||||
LiveWorship = 11
|
||||
Lyrix = 12
|
||||
MediaShout = 13
|
||||
OpenSong = 14
|
||||
OPSPro = 15
|
||||
PowerPraise = 16
|
||||
PowerSong = 17
|
||||
PresentationManager = 18
|
||||
ProPresenter = 19
|
||||
SingingTheFaith = 20
|
||||
SongBeamer = 21
|
||||
SongPro = 22
|
||||
SongShowPlus = 23
|
||||
SongsOfFellowship = 24
|
||||
SundayPlus = 25
|
||||
VideoPsalm = 26
|
||||
WordsOfWorship = 27
|
||||
WorshipAssistant = 28
|
||||
WorshipCenterPro = 29
|
||||
ZionWorx = 30
|
||||
Datasoul = 31
|
||||
EasyWorshipServiceSqliteDB = 10
|
||||
FoilPresenter = 11
|
||||
LiveWorship = 12
|
||||
Lyrix = 13
|
||||
MediaShout = 14
|
||||
OpenSong = 15
|
||||
OPSPro = 16
|
||||
PowerPraise = 17
|
||||
PowerSong = 18
|
||||
PresentationManager = 19
|
||||
ProPresenter = 20
|
||||
SingingTheFaith = 21
|
||||
SongBeamer = 22
|
||||
SongPro = 23
|
||||
SongShowPlus = 24
|
||||
SongsOfFellowship = 25
|
||||
SundayPlus = 26
|
||||
VideoPsalm = 27
|
||||
WordsOfWorship = 28
|
||||
WorshipAssistant = 29
|
||||
WorshipCenterPro = 30
|
||||
ZionWorx = 31
|
||||
Datasoul = 32
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
|
@ -278,6 +279,14 @@ class SongFormat(object):
|
|||
'filter': '{text} (*.ews)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship 2007/2009 Service File'))
|
||||
},
|
||||
EasyWorshipServiceSqliteDB: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship 6/7 Service File',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '{text} (*.ewsx)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship 6/7 Service File'))
|
||||
},
|
||||
FoilPresenter: {
|
||||
'class': FoilPresenterImport,
|
||||
'name': 'Foilpresenter',
|
||||
|
@ -487,6 +496,7 @@ class SongFormat(object):
|
|||
SongFormat.EasyWorshipDB,
|
||||
SongFormat.EasyWorshipSqliteDB,
|
||||
SongFormat.EasyWorshipService,
|
||||
SongFormat.EasyWorshipServiceSqliteDB,
|
||||
SongFormat.FoilPresenter,
|
||||
SongFormat.LiveWorship,
|
||||
SongFormat.Lyrix,
|
||||
|
|
|
@ -56,7 +56,7 @@ class DatasoulImport(SongImport):
|
|||
tree = objectify.parse(str(file_path), parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
song_xml = tree.getroot()
|
||||
if song_xml.tag != 'Song':
|
||||
|
|
|
@ -93,16 +93,16 @@ class DreamBeamImport(SongImport):
|
|||
parsed_file = etree.parse(xml_file, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
file_str = etree.tostring(parsed_file)
|
||||
if not file_str:
|
||||
log.exception('Could not find XML in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
xml = file_str.decode()
|
||||
song_xml = objectify.fromstring(xml)
|
||||
|
@ -151,7 +151,7 @@ class DreamBeamImport(SongImport):
|
|||
author_copyright = song_xml.Text2.Text.text
|
||||
if author_copyright:
|
||||
author_copyright = str(author_copyright)
|
||||
if author_copyright.find(SongStrings.CopyrightSymbol) >= 0:
|
||||
if author_copyright.find(SongStrings().CopyrightSymbol) >= 0:
|
||||
self.add_copyright(author_copyright)
|
||||
else:
|
||||
self.parse_author(author_copyright)
|
||||
|
|
|
@ -54,16 +54,16 @@ class EasySlidesImport(SongImport):
|
|||
parsed_file = etree.parse(xml_file, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
file_str = etree.tostring(parsed_file)
|
||||
if not file_str:
|
||||
log.exception('Could not find XML in file {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
xml = file_str.decode()
|
||||
song_xml = objectify.fromstring(xml)
|
||||
|
|
|
@ -28,6 +28,8 @@ import sqlite3
|
|||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding, strip_rtf
|
||||
|
@ -83,6 +85,8 @@ class EasyWorshipSongImport(SongImport):
|
|||
self.import_ews()
|
||||
elif ext == '.db':
|
||||
self.import_db()
|
||||
elif ext == '.ewsx':
|
||||
self.import_ewsx()
|
||||
else:
|
||||
self.import_sqlite_db()
|
||||
except Exception:
|
||||
|
@ -346,6 +350,65 @@ class EasyWorshipSongImport(SongImport):
|
|||
db_file.close()
|
||||
self.memo_file.close()
|
||||
|
||||
def import_ewsx(self):
|
||||
"""
|
||||
Imports songs from an EasyWorship 6/7 service file, which is just a zip file with an Sqlite DB with text
|
||||
resources. Non-text recources is also in the zip file, but is ignored.
|
||||
"""
|
||||
invalid_ewsx_msg = translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'This is not a valid Easy Worship 6/7 service file.')
|
||||
# Open ewsx file if it exists
|
||||
if not self.import_source.is_file():
|
||||
log.debug('Given ewsx file does not exists.')
|
||||
return
|
||||
tmp_db_file = NamedTemporaryFile(delete=False)
|
||||
with ZipFile(self.import_source, 'r') as eswx_file:
|
||||
db_zfile = eswx_file.open('main.db')
|
||||
# eswx has bad CRC for the database for some reason (custom CRC?), so skip the CRC
|
||||
db_zfile._expected_crc = None
|
||||
db_data = db_zfile.read()
|
||||
tmp_db_file.write(db_data)
|
||||
tmp_db_file.close()
|
||||
ewsx_conn = sqlite3.connect(tmp_db_file.file.name)
|
||||
if ewsx_conn is None:
|
||||
self.log_error(self.import_source, invalid_ewsx_msg)
|
||||
return
|
||||
ewsx_db = ewsx_conn.cursor()
|
||||
# Take a stab at how text is encoded
|
||||
self.encoding = 'cp1252'
|
||||
self.encoding = retrieve_windows_encoding(self.encoding)
|
||||
if not self.encoding:
|
||||
log.debug('No encoding set.')
|
||||
return
|
||||
# get list of songs in service file, presentation_type=6 means songs
|
||||
songs_exec = ewsx_db.execute('SELECT rowid, title, author, copyright, reference_number '
|
||||
'FROM presentation WHERE presentation_type=6;')
|
||||
songs = songs_exec.fetchall()
|
||||
for song in songs:
|
||||
self.title = title = song[1]
|
||||
self.author = song[2]
|
||||
self.copyright = song[3]
|
||||
self.ccli_number = song[4]
|
||||
# get slides for the song, element_type=6 means songs, element_style_type=4 means song text
|
||||
slides = ewsx_db.execute('SELECT rt.rtf '
|
||||
'FROM element as e '
|
||||
'JOIN slide as s ON e.slide_id = s.rowid '
|
||||
'JOIN resource_text as rt ON rt.resource_id = e.foreground_resource_id '
|
||||
'WHERE e.element_type=6 AND e.element_style_type=4 AND s.presentation_id = ? '
|
||||
'ORDER BY s.order_index;', (song[0],))
|
||||
for slide in slides:
|
||||
if slide:
|
||||
self.set_song_import_object(self.author, slide[0].encode())
|
||||
# save song
|
||||
if not self.finish():
|
||||
self.log_error(self.import_source,
|
||||
translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'"{title}" could not be imported. {entry}').
|
||||
format(title=title, entry=self.entry_error_log))
|
||||
# close database handles
|
||||
ewsx_conn.close()
|
||||
Path(tmp_db_file.file.name).unlink()
|
||||
|
||||
def import_sqlite_db(self):
|
||||
"""
|
||||
Import the songs from an EasyWorship 6 SQLite database
|
||||
|
|
|
@ -125,7 +125,7 @@ class FoilPresenterImport(SongImport):
|
|||
xml = etree.tostring(parsed_file).decode()
|
||||
self.foil_presenter.xml_to_song(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
log.exception('XML syntax error in file {path}'.format(path=file_path))
|
||||
except AttributeError:
|
||||
self.log_error(file_path, translate('SongsPlugin.FoilPresenterSongImport',
|
||||
|
|
|
@ -72,7 +72,7 @@ class LiveWorshipImport(SongImport):
|
|||
try:
|
||||
self.root = etree.fromstring(xml_content, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
self.log_error(self.dump_file, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.dump_file, SongStrings().XMLSyntaxError)
|
||||
log.exception('XML syntax error in file {path}'.format(path=str(self.dump_file)))
|
||||
|
||||
def extract_songs(self):
|
||||
|
|
|
@ -73,7 +73,7 @@ class OpenLyricsImport(SongImport):
|
|||
self.open_lyrics.xml_to_song(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {path}'.format(path=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
except OpenLyricsError as exception:
|
||||
log.exception('OpenLyricsException {error:d} in file {name}: {text}'.format(error=exception.type,
|
||||
name=file_path,
|
||||
|
|
|
@ -129,7 +129,7 @@ class OpenSongImport(SongImport):
|
|||
try:
|
||||
tree = objectify.parse(file)
|
||||
except (etree.Error, etree.LxmlError):
|
||||
self.log_error(file.name, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file.name, SongStrings().XMLSyntaxError)
|
||||
log.exception('Error parsing XML')
|
||||
return
|
||||
root = tree.getroot()
|
||||
|
|
|
@ -50,11 +50,11 @@ class PowerPraiseImport(SongImport):
|
|||
root = objectify.parse(xml_file).getroot()
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
self.process_song(root, file_path)
|
||||
|
||||
|
|
|
@ -54,11 +54,11 @@ class ProPresenterImport(SongImport):
|
|||
root = objectify.parse(xml_file).getroot()
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
try:
|
||||
self.process_song(root, file_path)
|
||||
|
|
|
@ -104,7 +104,7 @@ class SongImport(QtCore.QObject):
|
|||
self.verse_counts = {}
|
||||
self.copyright_string = translate('SongsPlugin.SongImport', 'copyright')
|
||||
|
||||
def log_error(self, file_path, reason=SongStrings.SongIncomplete):
|
||||
def log_error(self, file_path, reason=SongStrings().SongIncomplete):
|
||||
"""
|
||||
This should be called, when a song could not be imported.
|
||||
|
||||
|
@ -151,11 +151,11 @@ class SongImport(QtCore.QObject):
|
|||
:param text: Some text
|
||||
"""
|
||||
lines = text.split('\n')
|
||||
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings.CopyrightSymbol)) >= 0:
|
||||
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings().CopyrightSymbol)) >= 0:
|
||||
copyright_found = False
|
||||
for line in lines:
|
||||
if (copyright_found or line.lower().find(self.copyright_string) >= 0 or
|
||||
line.find(str(SongStrings.CopyrightSymbol)) >= 0):
|
||||
line.find(str(SongStrings().CopyrightSymbol)) >= 0):
|
||||
copyright_found = True
|
||||
self.add_copyright(line)
|
||||
else:
|
||||
|
|
|
@ -124,7 +124,7 @@ class SongMediaItem(MediaManagerItem):
|
|||
def retranslate_ui(self):
|
||||
self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
|
||||
self.search_text_button.setText(UiStrings().Search)
|
||||
self.maintenance_action.setText(SongStrings.SongMaintenance)
|
||||
self.maintenance_action.setText(SongStrings().SongMaintenance)
|
||||
self.maintenance_action.setToolTip(translate('SongsPlugin.MediaItem',
|
||||
'Maintain the lists of authors, topics and books.'))
|
||||
|
||||
|
@ -145,11 +145,11 @@ class SongMediaItem(MediaManagerItem):
|
|||
(SongSearch.Lyrics, UiIcons().search_lyrics,
|
||||
translate('SongsPlugin.MediaItem', 'Lyrics'),
|
||||
translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
|
||||
(SongSearch.Authors, UiIcons().user, SongStrings.Authors,
|
||||
(SongSearch.Authors, UiIcons().user, SongStrings().Authors,
|
||||
translate('SongsPlugin.MediaItem', 'Search Authors...')),
|
||||
(SongSearch.Topics, UiIcons().light_bulb, SongStrings.Topics,
|
||||
(SongSearch.Topics, UiIcons().light_bulb, SongStrings().Topics,
|
||||
translate('SongsPlugin.MediaItem', 'Search Topics...')),
|
||||
(SongSearch.Books, UiIcons().address, SongStrings.SongBooks,
|
||||
(SongSearch.Books, UiIcons().address, SongStrings().SongBooks,
|
||||
translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
|
||||
(SongSearch.Themes, UiIcons().theme, UiStrings().Themes, UiStrings().SearchThemes),
|
||||
(SongSearch.Copyright, UiIcons().copyright,
|
||||
|
@ -674,7 +674,7 @@ class SongMediaItem(MediaManagerItem):
|
|||
authors=create_separated_list(authors.translation))
|
||||
)
|
||||
if song.copyright:
|
||||
item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
|
||||
item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings().CopyrightSymbol,
|
||||
song=song.copyright))
|
||||
if song.songbook_entries:
|
||||
item.raw_footer.append(", ".join(songbooks))
|
||||
|
|
|
@ -32,18 +32,19 @@ class SongStrings(object):
|
|||
"""
|
||||
Provide standard strings for use throughout the songs plugin.
|
||||
"""
|
||||
# These strings should need a good reason to be retranslated elsewhere.
|
||||
Author = translate('OpenLP.Ui', 'Author', 'Singular')
|
||||
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
|
||||
AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
|
||||
CopyrightSymbol = '\xa9'
|
||||
SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular')
|
||||
SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural')
|
||||
SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found')
|
||||
SongMaintenance = translate('OpenLP.Ui', 'Song Maintenance')
|
||||
Topic = translate('OpenLP.Ui', 'Topic', 'Singular')
|
||||
Topics = translate('OpenLP.Ui', 'Topics', 'Plural')
|
||||
XMLSyntaxError = translate('OpenLP.Ui', 'XML syntax error')
|
||||
def __init__(self):
|
||||
# These strings should need a good reason to be retranslated elsewhere.
|
||||
self.Author = translate('OpenLP.Ui', 'Author', 'Singular')
|
||||
self.Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
|
||||
self.AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
|
||||
self.CopyrightSymbol = '\xa9'
|
||||
self.SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular')
|
||||
self.SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural')
|
||||
self.SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found')
|
||||
self.SongMaintenance = translate('OpenLP.Ui', 'Song Maintenance')
|
||||
self.Topic = translate('OpenLP.Ui', 'Topic', 'Singular')
|
||||
self.Topics = translate('OpenLP.Ui', 'Topics', 'Plural')
|
||||
self.XMLSyntaxError = translate('OpenLP.Ui', 'XML syntax error')
|
||||
|
||||
|
||||
def show_key_warning(parent):
|
||||
|
|
|
@ -107,8 +107,7 @@
|
|||
</column>
|
||||
<item row="0" column="0">
|
||||
<property name="text">
|
||||
<string>
|
||||
Authors</string>
|
||||
<string>Authors</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
|
@ -120,8 +119,7 @@ Authors</string>
|
|||
</item>
|
||||
<item row="1" column="0">
|
||||
<property name="text">
|
||||
<string>
|
||||
Topics</string>
|
||||
<string>Topics</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
|
@ -133,8 +131,7 @@ Topics</string>
|
|||
</item>
|
||||
<item row="2" column="0">
|
||||
<property name="text">
|
||||
<string>
|
||||
Books/Hymnals</string>
|
||||
<string>Books/Hymnals</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
|
|
|
@ -95,6 +95,7 @@ MODULES = [
|
|||
'pymediainfo',
|
||||
'vlc',
|
||||
'qrcode',
|
||||
'packaging',
|
||||
]
|
||||
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -105,6 +105,7 @@ using a computer and a display/projector.""",
|
|||
'flask-cors',
|
||||
'lxml',
|
||||
'Mako',
|
||||
'packaging',
|
||||
'platformdirs',
|
||||
'PyICU',
|
||||
'pymediainfo >= 2.2',
|
||||
|
|
|
@ -29,7 +29,7 @@ from openlp.core.state import State
|
|||
from openlp.core.lib.plugin import PluginStatus, StringContent
|
||||
|
||||
|
||||
def test_plugins_returns_list(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_plugins_returns_list(flask_client: FlaskClient):
|
||||
State().load_settings()
|
||||
res = flask_client.get('/api/v2/core/plugins').get_json()
|
||||
assert len(res) == 0
|
||||
|
@ -52,14 +52,30 @@ def test_plugins_returns_list(flask_client: FlaskClient, registry: Registry, set
|
|||
assert res[0]['name'] == plugin.text_strings[StringContent.Name]['plural']
|
||||
|
||||
|
||||
def test_system_information(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_system_information(flask_client: FlaskClient, settings: Settings):
|
||||
Registry().get('settings_thread').setValue('api/authentication enabled', False)
|
||||
res = flask_client.get('/api/v2/core/system').get_json()
|
||||
assert res['websocket_port'] > 0
|
||||
assert not res['login_required']
|
||||
|
||||
|
||||
def test_poll_backend(registry: Registry, settings: Settings):
|
||||
def test_shortcuts(flask_client: FlaskClient, settings: Settings):
|
||||
action = 'shortcuts/aboutItem'
|
||||
shortcut = 'Ctrl+F1'
|
||||
Registry().get('settings_thread').setValue(action, shortcut)
|
||||
res = flask_client.get('/api/v2/core/shortcuts')
|
||||
assert res.status_code == 200
|
||||
assert res.get_json()[0]['action'] == action.removeprefix('shortcuts/')
|
||||
assert res.get_json()[0]['shortcut'] == shortcut
|
||||
|
||||
|
||||
def test_language(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.get('/api/v2/core/language')
|
||||
assert res.status_code == 200
|
||||
assert res.get_json()['language']
|
||||
|
||||
|
||||
def test_poll_backend(settings: Settings):
|
||||
"""
|
||||
Test the raw poll function returns the correct JSON
|
||||
"""
|
||||
|
@ -100,12 +116,12 @@ def test_login_without_data_returns_400(flask_client: FlaskClient):
|
|||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_login_with_invalid_credetials_returns_401(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_login_with_invalid_credetials_returns_401(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='invalid'))
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_login_with_valid_credetials_returns_token(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_login_with_valid_credetials_returns_token(flask_client: FlaskClient, settings: Settings):
|
||||
Registry().register('authentication_token', 'foobar')
|
||||
res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='password'))
|
||||
assert res.status_code == 200
|
||||
|
@ -125,7 +141,7 @@ def test_retrieving_image(flask_client: FlaskClient):
|
|||
assert res['binary_image'] != ''
|
||||
|
||||
|
||||
def test_toggle_display_requires_login(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_requires_login(flask_client: FlaskClient, settings: Settings):
|
||||
settings.setValue('api/authentication enabled', True)
|
||||
Registry().register('authentication_token', 'foobar')
|
||||
res = flask_client.post('/api/v2/core/display')
|
||||
|
@ -138,18 +154,17 @@ def test_toggle_display_does_not_allow_get(flask_client: FlaskClient):
|
|||
assert res.status_code == 405
|
||||
|
||||
|
||||
def test_toggle_display_invalid_action(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_invalid_action(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/display', json={'display': 'foo'})
|
||||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_toggle_display_no_data(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_no_data(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/display', json={})
|
||||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClient, registry: Registry,
|
||||
settings: Settings):
|
||||
def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClient, settings: Settings):
|
||||
class FakeController:
|
||||
class Emitter:
|
||||
def emit(self, value):
|
||||
|
@ -162,7 +177,7 @@ def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClien
|
|||
assert controller.slidecontroller_toggle_display.set == 'show'
|
||||
|
||||
|
||||
def test_cors_headers_are_present(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_cors_headers_are_present(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.get('/api/v2/core/system')
|
||||
assert 'Access-Control-Allow-Origin' in res.headers
|
||||
assert res.headers['Access-Control-Allow-Origin'] == '*'
|
||||
|
|
|
@ -19,15 +19,15 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
|
||||
# Mock QtWebEngineWidgets
|
||||
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
|
||||
|
||||
|
@ -37,14 +37,20 @@ from openlp.core.common.settings import Settings
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def app_main_env():
|
||||
def mocked_qapp():
|
||||
patcher = patch('openlp.core.app.QtWidgets.QApplication')
|
||||
yield patcher.start()
|
||||
patcher.stop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_main_env(mocked_qapp):
|
||||
with patch('openlp.core.app.Settings') as mock_settings, \
|
||||
patch('openlp.core.app.Registry') as mock_registry, \
|
||||
patch('openlp.core.app.AppLocation') as mock_apploc, \
|
||||
patch('openlp.core.app.LanguageManager'), \
|
||||
patch('openlp.core.app.qInitResources'), \
|
||||
patch('openlp.core.app.parse_options'), \
|
||||
patch('openlp.core.app.QtWidgets.QApplication') as mock_qapp, \
|
||||
patch('openlp.core.app.parse_options') as mocked_parse_options, \
|
||||
patch('openlp.core.app.QtWidgets.QMessageBox.warning') as mock_warn, \
|
||||
patch('openlp.core.app.QtWidgets.QMessageBox.information'), \
|
||||
patch('openlp.core.app.OpenLP') as mock_openlp, \
|
||||
|
@ -58,7 +64,10 @@ def app_main_env():
|
|||
openlp_server.is_another_instance_running.return_value = False
|
||||
mock_apploc.get_data_path.return_value = Path()
|
||||
mock_apploc.get_directory.return_value = Path()
|
||||
mock_qapp.return_value.devicePixelRatio.return_value = 1.0
|
||||
mocked_parse_options.return_value = Namespace(no_error_form=False, loglevel='warning', portable=False,
|
||||
portablepath=None, no_web_server=False, display_custom_path=None,
|
||||
rargs=[])
|
||||
mocked_qapp.return_value.devicePixelRatio.return_value = 1.0
|
||||
mock_warn.return_value = True
|
||||
openlp_instance = MagicMock()
|
||||
mock_openlp.return_value = openlp_instance
|
||||
|
@ -311,7 +320,8 @@ def test_backup_on_upgrade(mocked_question, mocked_get_version, qapp, settings):
|
|||
@patch('openlp.core.app.backup_if_version_changed')
|
||||
@patch('openlp.core.app.set_up_web_engine_cache')
|
||||
@patch('openlp.core.app.set_up_logging')
|
||||
def test_main(mock_logging, mock_web_cache, mock_backup, mock_sys, mock_openlp, app_main_env):
|
||||
def test_main(mock_logging: MagicMock, mock_web_cache: MagicMock, mock_backup: MagicMock, mock_sys: MagicMock,
|
||||
mock_openlp: MagicMock, mocked_qapp: MagicMock, app_main_env: None):
|
||||
"""
|
||||
Test the main method performs primary actions
|
||||
"""
|
||||
|
@ -329,6 +339,9 @@ def test_main(mock_logging, mock_web_cache, mock_backup, mock_sys, mock_openlp,
|
|||
mock_logging.assert_called_once()
|
||||
mock_web_cache.assert_called_once()
|
||||
mock_sys.exit.assert_called_once()
|
||||
mocked_qapp.setOrganizationName.assert_called_once_with('OpenLP')
|
||||
mocked_qapp.setApplicationName.assert_called_once_with('OpenLP')
|
||||
mocked_qapp.setOrganizationDomain.assert_called_once_with('openlp.org')
|
||||
|
||||
|
||||
@patch('openlp.core.app.QtWidgets.QMessageBox.warning')
|
||||
|
|
|
@ -135,7 +135,7 @@ def test_worker_start_nightly_version(mock_get_web_page, mock_platform):
|
|||
"""
|
||||
# GIVEN: A last check date, current version, and an instance of worker
|
||||
last_check_date = '1970-01-01'
|
||||
current_version = {'full': '2.1-bzr2345', 'version': '2.1', 'build': '2345'}
|
||||
current_version = {'full': '2.1.0+git2345', 'version': '2.1', 'build': '2345'}
|
||||
mock_platform.system.return_value = 'Linux'
|
||||
mock_platform.release.return_value = '4.12.0-1-amd64'
|
||||
mock_get_web_page.return_value = '2.4.6'
|
||||
|
@ -148,7 +148,7 @@ def test_worker_start_nightly_version(mock_get_web_page, mock_platform):
|
|||
|
||||
# THEN: The check completes and the signal is emitted
|
||||
expected_download_url = 'https://get.openlp.org/versions/nightly_version.txt'
|
||||
expected_headers = {'User-Agent': 'OpenLP/2.1-bzr2345 Linux/4.12.0-1-amd64; '}
|
||||
expected_headers = {'User-Agent': 'OpenLP/2.1.0+git2345 Linux/4.12.0-1-amd64; '}
|
||||
mock_get_web_page.assert_called_once_with(expected_download_url, headers=expected_headers)
|
||||
mock_new_version.emit.assert_called_once_with('2.4.6')
|
||||
mock_quit.emit.assert_called_once_with()
|
||||
|
@ -162,7 +162,7 @@ def test_worker_empty_response(mock_get_web_page, mock_platform):
|
|||
"""
|
||||
# GIVEN: A last check date, current version, and an instance of worker
|
||||
last_check_date = '1970-01-01'
|
||||
current_version = {'full': '2.1-bzr2345', 'version': '2.1', 'build': '2345'}
|
||||
current_version = {'full': '2.1+git2345', 'version': '2.1', 'build': '2345'}
|
||||
mock_platform.system.return_value = 'Linux'
|
||||
mock_platform.release.return_value = '4.12.0-1-amd64'
|
||||
mock_get_web_page.return_value = '\n'
|
||||
|
@ -175,7 +175,7 @@ def test_worker_empty_response(mock_get_web_page, mock_platform):
|
|||
|
||||
# THEN: The check completes and the signal is emitted
|
||||
expected_download_url = 'https://get.openlp.org/versions/nightly_version.txt'
|
||||
expected_headers = {'User-Agent': 'OpenLP/2.1-bzr2345 Linux/4.12.0-1-amd64; '}
|
||||
expected_headers = {'User-Agent': 'OpenLP/2.1+git2345 Linux/4.12.0-1-amd64; '}
|
||||
mock_get_web_page.assert_called_once_with(expected_download_url, headers=expected_headers)
|
||||
assert mock_new_version.emit.call_count == 0
|
||||
mock_quit.emit.assert_called_once_with()
|
||||
|
|
|
@ -21,10 +21,16 @@
|
|||
"""
|
||||
This module contains tests for the lib submodule of the Songs plugin.
|
||||
"""
|
||||
import logging
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib.db import Author
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -33,6 +39,17 @@ def edit_song_form():
|
|||
return EditSongForm(None, MagicMock(), MagicMock())
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def edit_song_form_with_ui(settings: Settings) -> EditSongForm:
|
||||
main_window = QtWidgets.QMainWindow()
|
||||
Registry().register('main_window', main_window)
|
||||
Registry().register('theme_manager', MagicMock())
|
||||
form = EditSongForm(None, main_window, MagicMock())
|
||||
yield form
|
||||
del form
|
||||
del main_window
|
||||
|
||||
|
||||
def test_validate_matching_tags(edit_song_form):
|
||||
# Given a set of tags
|
||||
tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']
|
||||
|
@ -86,3 +103,104 @@ def test_load_objects(mocked_set_case_insensitive_completer, edit_song_form, set
|
|||
mocked_set_case_insensitive_completer.assert_called_once_with(mocked_cache, mocked_combo)
|
||||
mocked_combo.setCurrentIndex.assert_called_once_with(-1)
|
||||
mocked_combo.setCurrentText.assert_called_once_with('')
|
||||
|
||||
|
||||
def test_add_multiple_audio_from_file(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that not more than one Linked Audio item can be added
|
||||
"""
|
||||
# GIVEN: A Linked Audio list with 1 item and mocked error message handler
|
||||
item = QtWidgets.QListWidgetItem('Audio file')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Add File is clicked
|
||||
edit_song_form_with_ui.on_audio_add_from_file_button_clicked()
|
||||
|
||||
# THEN: A call to show an error message should have been made and no items should have been added
|
||||
mocked_error_message.assert_called_once()
|
||||
assert edit_song_form_with_ui.audio_list_widget.count() == 1
|
||||
|
||||
|
||||
def test_add_multiple_audio_from_media(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that not more than one Linked Audio item can be added
|
||||
"""
|
||||
# GIVEN: A Linked Audio list with 1 item and mocked error message handler
|
||||
item = QtWidgets.QListWidgetItem('Audio file')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Add Media is clicked
|
||||
edit_song_form_with_ui.on_audio_add_from_media_button_clicked()
|
||||
|
||||
# THEN: A call to show an error message should have been made and no items should have been added
|
||||
mocked_error_message.assert_called_once()
|
||||
assert edit_song_form_with_ui.audio_list_widget.count() == 1
|
||||
|
||||
|
||||
def test_validate_song_multiple_audio(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that a form with multiple Linked Audio items does not pass validation
|
||||
"""
|
||||
# GIVEN: A form with title, lyrics, an author, and 2 Linked Audio items
|
||||
edit_song_form_with_ui.title_edit.setText('Song Title')
|
||||
verse_def = '{tag}{number}'.format(tag=VerseType.tags[0], number=1)
|
||||
song_lyrics = 'Song Lyrics'
|
||||
verse_item = QtWidgets.QTableWidgetItem(song_lyrics)
|
||||
verse_item.setData(QtCore.Qt.UserRole, verse_def)
|
||||
verse_item.setText(song_lyrics)
|
||||
edit_song_form_with_ui.verse_list_widget.setRowCount(1)
|
||||
edit_song_form_with_ui.verse_list_widget.setItem(0, 0, verse_item)
|
||||
item_1 = QtWidgets.QListWidgetItem('Audio file 1')
|
||||
item_2 = QtWidgets.QListWidgetItem('Audio file 2')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_1)
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_2)
|
||||
author = Author(first_name='', last_name='', display_name='Author')
|
||||
author_type = edit_song_form_with_ui.author_types_combo_box.itemData(0)
|
||||
edit_song_form_with_ui._add_author_to_list(author, author_type)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Song is validated
|
||||
song_valid = edit_song_form_with_ui._validate_song()
|
||||
|
||||
# THEN: It should not be valid
|
||||
assert song_valid is False
|
||||
|
||||
|
||||
def test_validate_song_one_audio(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that a form with one Linked Audio item passes validation
|
||||
"""
|
||||
# GIVEN: A form with title, lyrics, an author, and 1 Linked Audio item
|
||||
edit_song_form_with_ui.title_edit.setText('Song Title')
|
||||
verse_def = '{tag}{number}'.format(tag=VerseType.tags[0], number=1)
|
||||
song_lyrics = 'Song Lyrics'
|
||||
verse_item = QtWidgets.QTableWidgetItem(song_lyrics)
|
||||
verse_item.setData(QtCore.Qt.UserRole, verse_def)
|
||||
verse_item.setText(song_lyrics)
|
||||
edit_song_form_with_ui.verse_list_widget.setRowCount(1)
|
||||
edit_song_form_with_ui.verse_list_widget.setItem(0, 0, verse_item)
|
||||
item_1 = QtWidgets.QListWidgetItem('Audio file 1')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_1)
|
||||
author = Author(first_name='', last_name='', display_name='Author')
|
||||
author_type = edit_song_form_with_ui.author_types_combo_box.itemData(0)
|
||||
edit_song_form_with_ui._add_author_to_list(author, author_type)
|
||||
# If the validation does fail for some reason it will likely try to display an error message,
|
||||
# so make sure error_message exists to avoid an error
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Song is validated
|
||||
song_valid = edit_song_form_with_ui._validate_song()
|
||||
|
||||
# Log the error message to help determine the cause in the case validation failed
|
||||
if mocked_error_message.called:
|
||||
_title, message = mocked_error_message.call_args.args
|
||||
logging.error('Validation error message: {message}'.format(message=message))
|
||||
|
||||
# THEN: It should be valid
|
||||
assert song_valid is True
|
||||
|
|
|
@ -498,6 +498,48 @@ def test_ews_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongIm
|
|||
mocked_finish.assert_called_with()
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.SongImport')
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding')
|
||||
def test_ewsx_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongImport: MagicMock,
|
||||
registry: Registry, settings: Settings):
|
||||
"""
|
||||
Test the actual import of song from ewsx file and check that the imported data is correct.
|
||||
"""
|
||||
|
||||
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
|
||||
# and mocked out "author", "add_copyright", "add_verse", "finish" methods.
|
||||
mocked_retrieve_windows_encoding.return_value = 'cp1252'
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
mocked_add_author = MagicMock()
|
||||
mocked_add_verse = MagicMock()
|
||||
mocked_finish = MagicMock()
|
||||
mocked_title = MagicMock()
|
||||
mocked_finish.return_value = True
|
||||
importer = EasyWorshipSongImportLogger(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = False
|
||||
importer.add_author = mocked_add_author
|
||||
importer.add_verse = mocked_add_verse
|
||||
importer.title = mocked_title
|
||||
importer.finish = mocked_finish
|
||||
importer.topics = []
|
||||
|
||||
# WHEN: Importing ews file
|
||||
importer.import_source = str(TEST_PATH / 'test1.ewsx')
|
||||
import_result = importer.do_import()
|
||||
|
||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||
# called.
|
||||
title = EWS_SONG_TEST_DATA['title']
|
||||
assert import_result is None, 'do_import should return None when it has completed'
|
||||
assert title in importer._title_assignment_list, 'title for should be "%s"' % title
|
||||
mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0])
|
||||
for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']:
|
||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||
mocked_finish.assert_called_with()
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.SongImport')
|
||||
def test_import_rtf_unescaped_unicode(MockSongImport: MagicMock, registry: Registry, settings: Settings):
|
||||
"""
|
||||
|
|
|
@ -111,14 +111,14 @@ def test_can_parse_file_having_a_processing_instruction(mocked_logger: MagicMock
|
|||
# otherwise we don't care about it now (but should in other tests...)
|
||||
assert ex is not etree.XMLSyntaxError
|
||||
|
||||
# THEN: the importer's log_error method was never called with SongStrings.XMLSyntaxError as its second
|
||||
# THEN: the importer's log_error method was never called with SongStrings().XMLSyntaxError as its second
|
||||
# positional argument
|
||||
if importer.log_error.called:
|
||||
for call_args in importer.log_error.call_args_list:
|
||||
args = call_args[0]
|
||||
# there are at least two positional arguments
|
||||
if len(args) > 1:
|
||||
assert args[1] is not SongStrings.XMLSyntaxError
|
||||
assert args[1] is not SongStrings().XMLSyntaxError
|
||||
|
||||
# THEN: the logger's 'exception' method was never called with a first positional argument
|
||||
# which is a string and starts with 'XML syntax error in file'
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue