Merge branch 'master' into openlp-migrate-to-pyside6

This commit is contained in:
Tomas Groth 2024-04-26 17:24:25 +02:00
commit eba83f1ab9
29 changed files with 389 additions and 96 deletions

View File

@ -39,7 +39,7 @@ def index(path):
'index.html', mimetype='text/html')
@main_views.route('/assets/<path>')
@main_views.route('/assets/<path:path>')
def assets(path):
return send_from_directory(str(AppLocation.get_section_data_path('remotes') / 'assets'),
path, mimetype=get_mime_type(path))

View File

@ -24,6 +24,7 @@ from flask import jsonify, request, abort, Blueprint
from PySide6 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
@ -82,6 +83,12 @@ def system_information():
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

View File

@ -275,6 +275,8 @@ def sha256_file_hash(filename):
"""
Returns the hashed output of sha256 on the file content using Python3 hashlib
This method allows PermissionError to bubble up, while supressing other exceptions
:param filename: Name of the file to hash
:returns: str
"""
@ -288,6 +290,8 @@ def sha256_file_hash(filename):
hash_obj.update(chunk)
return hash_obj.hexdigest()
except PermissionError:
raise
except Exception:
return None

View File

@ -356,10 +356,12 @@ class UiStrings(metaclass=Singleton):
self.BibleNoBibles = translate('OpenLP.Ui', '<strong>There are no Bibles currently installed.</strong><br><br>'
'Please use the Import Wizard to install one or more Bibles.')
self.Bottom = translate('OpenLP.Ui', 'Bottom')
self.Bridge = translate('SongsPlugin.VerseType', 'Bridge')
self.Browse = translate('OpenLP.Ui', 'Browse...')
self.Cancel = translate('OpenLP.Ui', 'Cancel')
self.CCLINumberLabel = translate('OpenLP.Ui', 'CCLI number:')
self.CCLISongNumberLabel = translate('OpenLP.Ui', 'CCLI song number:')
self.Chorus = translate('SongsPlugin.VerseType', 'Chorus')
self.CreateService = translate('OpenLP.Ui', 'Create a new service.')
self.ConfirmDelete = translate('OpenLP.Ui', 'Confirm Delete')
self.Continuous = translate('OpenLP.Ui', 'Continuous')
@ -371,14 +373,20 @@ class UiStrings(metaclass=Singleton):
'.html#strftime-strptime-behavior for more information.')
self.Delete = translate('OpenLP.Ui', '&Delete')
self.DisplayStyle = translate('OpenLP.Ui', 'Display style:')
self.Down = translate('SongsPlugin.EditVerseForm', 'Down')
self.Duplicate = translate('OpenLP.Ui', 'Duplicate Error')
self.Edit = translate('OpenLP.Ui', '&Edit')
self.EditVerse = translate('SongsPlugin.EditVerseForm', 'Edit Verse')
self.EmptyField = translate('OpenLP.Ui', 'Empty Field')
self.Ending = translate('SongsPlugin.VerseType', 'Ending')
self.Error = translate('OpenLP.Ui', 'Error')
self.Export = translate('OpenLP.Ui', 'Export')
self.File = translate('OpenLP.Ui', 'File')
self.FileCorrupt = translate('OpenLP.Ui', 'File appears to be corrupt.')
self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font point size unit')
self.ForcedSplit = translate('SongsPlugin.EditVerseForm', '&Forced Split')
self.ForcedSplitToolTip = translate('SongsPlugin.EditVerseForm', 'Split the verse when displayed '
'regardless of the screen size.')
self.Help = translate('OpenLP.Ui', 'Help')
self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')
@ -386,6 +394,10 @@ class UiStrings(metaclass=Singleton):
self.IFSp = translate('OpenLP.Ui', 'Invalid Files Selected', 'Plural')
self.Image = translate('OpenLP.Ui', 'Image')
self.Import = translate('OpenLP.Ui', 'Import')
self.Insert = translate('SongsPlugin.EditVerseForm', '&Insert')
self.InsertToolTip = translate('SongsPlugin.EditVerseForm',
'Split a slide into two by inserting a verse splitter.')
self.Intro = translate('SongsPlugin.VerseType', 'Intro')
self.LayoutStyle = translate('OpenLP.Ui', 'Layout style:')
self.Live = translate('OpenLP.Ui', 'Live')
self.LiveStream = translate('OpenLP.Ui', 'Live Stream')
@ -414,8 +426,11 @@ class UiStrings(metaclass=Singleton):
self.OpenService = translate('OpenLP.Ui', 'Open service.')
self.OptionalShowInFooter = translate('OpenLP.Ui', 'Optional, this will be displayed in footer.')
self.OptionalHideInFooter = translate('OpenLP.Ui', 'Optional, this won\'t be displayed in footer.')
self.Other = translate('SongsPlugin.VerseType', 'Other')
self.PermissionError = translate('OpenLP.Ui', 'Permission Error')
self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')
self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End')
self.PreChorus = translate('SongsPlugin.VerseType', 'Pre-Chorus')
self.Preview = translate('OpenLP.Ui', 'Preview')
self.PreviewToolbar = translate('OpenLP.Ui', 'Preview Toolbar')
self.PrintService = translate('OpenLP.Ui', 'Print Service')
@ -431,6 +446,11 @@ class UiStrings(metaclass=Singleton):
self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
self.SaveAndClose = translate('OpenLP.ui', translate('SongsPlugin.EditSongForm', '&Save && Close'))
self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
self.ScreenSetupHasChangedTitle = translate('OpenLP.MainWindow', 'Screen setup has changed')
self.ScreenSetupHasChanged = translate('OpenLP.MainWindow',
'The screen setup has changed. OpenLP will try to '
'automatically select a display screen, but '
'you should consider updating the screen settings.')
self.Search = translate('OpenLP.Ui', 'Search')
self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')
self.SelectDelete = translate('OpenLP.Ui', 'You must select an item to delete.')
@ -449,9 +469,16 @@ class UiStrings(metaclass=Singleton):
self.Themes = translate('OpenLP.Ui', 'Themes', 'Plural')
self.Tools = translate('OpenLP.Ui', 'Tools')
self.Top = translate('OpenLP.Ui', 'Top')
self.Transpose = translate('SongsPlugin.EditVerseForm', 'Transpose:')
self.UnableToRead = translate('OpenLP.Ui', 'Unable to read the file(s) listed below, please check that '
'your user has permission to read the file(s) or that the '
'file(s) are not using cloud storage (e.g. Dropbox, OneDrive).')
self.UnsupportedFile = translate('OpenLP.Ui', 'Unsupported File')
self.Up = translate('SongsPlugin.EditVerseForm', 'Up')
self.Verse = translate('SongsPlugin.VerseType', 'Verse')
self.VersePerSlide = translate('OpenLP.Ui', 'Verse Per Slide')
self.VersePerLine = translate('OpenLP.Ui', 'Verse Per Line')
self.VerseType = translate('SongsPlugin.EditVerseForm', '&Verse type:')
self.Version = translate('OpenLP.Ui', 'Version')
self.View = translate('OpenLP.Ui', 'View')
self.ViewMode = translate('OpenLP.Ui', 'View Mode')

View File

@ -35,6 +35,9 @@ from openlp.core.common.i18n import UiStrings, translate
log = logging.getLogger(__name__ + '.__init__')
DEFAULT_THUMBNAIL_HEIGHT = 88
class DataType(IntEnum):
U8 = 1
U16 = 2
@ -301,8 +304,8 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
:param Path image_path: The image file to create the icon from.
:param Path thumb_path: The filename to save the thumbnail to.
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
height of 88 is used.
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means it uses the value
from DEFAULT_THUMBNAIL_HEIGHT.
:return: The final icon.
"""
reader = QtGui.QImageReader(str(image_path))
@ -312,7 +315,7 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
ratio = 1
else:
ratio = reader.size().width() / reader.size().height()
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
reader.setScaledSize(QtCore.QSize(int(ratio * DEFAULT_THUMBNAIL_HEIGHT), DEFAULT_THUMBNAIL_HEIGHT))
elif size.isValid():
# Complete size given
reader.setScaledSize(size)
@ -330,7 +333,7 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
reader.setScaledSize(QtCore.QSize(int(ratio * size.height()), size.height()))
else:
# Invalid; use default height of 88
reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
reader.setScaledSize(QtCore.QSize(int(ratio * DEFAULT_THUMBNAIL_HEIGHT), DEFAULT_THUMBNAIL_HEIGHT))
thumb = reader.read()
thumb.save(str(thumb_path), thumb_path.suffix[1:].lower())
if not return_icon:

View File

@ -1040,12 +1040,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
and (datetime.now() - self.screen_change_timestamp).seconds < 5
should_show_messagebox = self.settings_form.isHidden() and not has_shown_messagebox_recently
if should_show_messagebox:
QtWidgets.QMessageBox.information(self, translate('OpenLP.MainWindow', 'Screen setup has changed'),
translate('OpenLP.MainWindow',
'The screen setup has changed. OpenLP will try to '
'automatically select a display screen, but '
'you should consider updating the screen settings.'),
QtWidgets.QMessageBox.StandardButton(
self.live_controller.toggle_display('desktop')
QtWidgets.QMessageBox.information(self,
UiStrings().ScreenSetupHasChangedTitle,
UiStrings().ScreenSetupHasChanged,
QtWidgets.QMessageBox.StandardButtons(
QtWidgets.QMessageBox.StandardButton.Ok))
self.screen_change_timestamp = datetime.now()
self.application.set_busy_cursor()

View File

@ -672,7 +672,12 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
missing_list = []
if not self._save_lite:
write_list, missing_list = self.get_write_file_list()
try:
write_list, missing_list = self.get_write_file_list()
except PermissionError as pe:
self.main_window.error_message(UiStrings.PermissionError,
UiStrings.UnableToRead + '\n\n' + pe.filename)
return False
if missing_list:
self.application.set_normal_cursor()
title = translate('OpenLP.ServiceManager', 'Service File(s) Missing')

View File

@ -1268,8 +1268,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
fallback_to_windowed = display_above_horizontal or display_above_vertical \
or display_beyond_horizontal or display_beyond_vertical
if fallback_to_windowed:
if self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay) or self.service_item.is_media() or \
self.service_item.is_command():
if self.service_item and (self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay) or
self.service_item.is_media() or self.service_item.is_command()):
if self.service_item.is_command():
# Attempting to get screenshot from command handler
service_item_name = self.service_item.name.lower()

View File

@ -162,11 +162,23 @@ def get_version():
except OSError:
log.exception('Error in version file.')
full_version = '0.0.0'
bits = full_version.split('.dev')
if '.dev' in full_version:
# Old way of doing build numbers, but also how hatch does them
version_number, build_number = full_version.split('.dev', 1)
build_number = build_number.split('+', 1)[1]
elif '+' in full_version:
# Current way of doing build numbers, may be replaced by hatch later
version_number, build_number = full_version.split('+', 1)
else:
# If this is a release, there is no build number
version_number = full_version
build_number = None
APPLICATION_VERSION = {
'full': full_version,
'version': bits[0],
'build': full_version.split('+')[1] if '+' in full_version else None
'version': version_number,
'build': build_number
}
if APPLICATION_VERSION['build']:
log.info('OpenLP version {version} build {build}'.format(version=APPLICATION_VERSION['version'],

View File

@ -27,6 +27,7 @@ import logging
from openlp.core.state import State
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.lib import build_icon
from openlp.core.db.manager import DBManager
from openlp.core.lib.plugin import Plugin, StringContent
@ -53,6 +54,7 @@ class CustomPlugin(Plugin):
self.weight = -5
self.db_manager = DBManager('custom', init_schema)
self.icon_path = UiIcons().custom
Registry().register('custom_manager', self.db_manager)
self.icon = build_icon(self.icon_path)
State().add_service(self.name, self.weight, is_plugin=True)
State().update_pre_conditions(self.name, self.check_pre_conditions())

View File

@ -121,6 +121,7 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog):
self.custom_slide.theme_name = self.theme_combo_box.currentText()
success = self.manager.save_object(self.custom_slide)
self.media_item.auto_select_id = self.custom_slide.id
Registry().execute('custom_changed', self.custom_slide.id)
return success
def on_up_button_clicked(self):

View File

@ -112,7 +112,7 @@ class CustomMediaItem(MediaManagerItem):
self.load_list(self.plugin.db_manager.get_all_objects(CustomSlide, order_by_ref=CustomSlide.title))
self.config_update()
def load_list(self, custom_slides, target_group=None):
def load_list(self, custom_slides=None, target_group=None):
# Sort out what custom we want to select after loading the list.
"""
@ -121,6 +121,8 @@ class CustomMediaItem(MediaManagerItem):
"""
self.save_auto_select_id()
self.list_view.clear()
if not custom_slides:
custom_slides = self.plugin.db_manager.get_all_objects(CustomSlide, order_by_ref=CustomSlide.title)
custom_slides.sort()
for custom_slide in custom_slides:
custom_name = QtWidgets.QListWidgetItem(custom_slide.title)
@ -201,6 +203,7 @@ class CustomMediaItem(MediaManagerItem):
id_list = [(item.data(QtCore.Qt.ItemDataRole.UserRole)) for item in self.list_view.selectedIndexes()]
for id in id_list:
self.plugin.db_manager.delete_object(CustomSlide, id)
Registry().execute('custom_deleted', id)
self.on_search_text_button_clicked()
def on_focus(self):
@ -257,6 +260,7 @@ class CustomMediaItem(MediaManagerItem):
credits=old_custom_slide.credits,
theme_name=old_custom_slide.theme_name)
self.plugin.db_manager.save_object(new_custom_slide)
Registry().execute('custom_changed', new_custom_slide.id)
self.on_search_text_button_clicked()
def on_search_text_button_clicked(self):

View File

@ -21,6 +21,7 @@
import logging
from pathlib import Path
from typing import Union
from PySide6 import QtCore, QtWidgets
@ -159,7 +160,11 @@ class ImageMediaItem(FolderLibraryItem):
if validate_thumb(file_path, thumbnail_path):
icon = build_icon(thumbnail_path)
else:
icon = create_thumb(file_path, thumbnail_path)
size: Union[QtCore.QSize, None] = None
slide_height: Union[int, None] = self.settings.value('advanced/slide max height')
if slide_height and slide_height > 0:
size = QtCore.QSize(-1, slide_height)
icon = create_thumb(file_path, thumbnail_path, size=size)
tree_item = QtWidgets.QTreeWidgetItem([file_name])
tree_item.setData(0, QtCore.Qt.ItemDataRole.UserRole, item)
tree_item.setIcon(0, icon)

View File

@ -1128,6 +1128,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
clean_song(self.manager, self.song)
self.manager.save_object(self.song)
self.media_item.auto_select_id = self.song.id
Registry().execute('song_changed', self.song.id)
def provide_help(self):
"""

View File

@ -21,7 +21,7 @@
from PySide6 import QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.i18n import UiStrings
from openlp.core.lib.ui import create_button_box
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.edits import SpellTextEdit
@ -87,8 +87,18 @@ class Ui_EditVerseDialog(object):
self.retranslate_ui(edit_verse_dialog)
def retranslate_ui(self, edit_verse_dialog):
edit_verse_dialog.setWindowTitle(translate('SongsPlugin.EditVerseForm', 'Edit Verse'))
self.verse_type_label.setText(translate('SongsPlugin.EditVerseForm', '&Verse type:'))
VerseType.translated_names = [
UiStrings().Verse,
UiStrings().Chorus,
UiStrings().Bridge,
UiStrings().PreChorus,
UiStrings().Intro,
UiStrings().Ending,
UiStrings().Other
]
VerseType.translated_tags = [name[0].lower() for name in VerseType.translated_names]
edit_verse_dialog.setWindowTitle(UiStrings().EditVerse)
self.verse_type_label.setText(UiStrings().VerseType)
self.verse_type_combo_box.setItemText(VerseType.Verse, VerseType.translated_names[VerseType.Verse])
self.verse_type_combo_box.setItemText(VerseType.Chorus, VerseType.translated_names[VerseType.Chorus])
self.verse_type_combo_box.setItemText(VerseType.Bridge, VerseType.translated_names[VerseType.Bridge])
@ -98,12 +108,10 @@ class Ui_EditVerseDialog(object):
self.verse_type_combo_box.setItemText(VerseType.Other, VerseType.translated_names[VerseType.Other])
self.overflow_split_button.setText(UiStrings().Split)
self.overflow_split_button.setToolTip(UiStrings().SplitToolTip)
self.forced_split_button.setText(translate('SongsPlugin.EditVerseForm', '&Forced Split'))
self.forced_split_button.setToolTip(translate('SongsPlugin.EditVerseForm', 'Split the verse when displayed '
'regardless of the screen size.'))
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
'Split a slide into two by inserting a verse splitter.'))
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))
self.forced_split_button.setText(UiStrings().ForcedSplit)
self.forced_split_button.setToolTip(UiStrings().ForcedSplitToolTip)
self.insert_button.setText(UiStrings().Insert)
self.insert_button.setToolTip(UiStrings().InsertToolTip)
self.transpose_label.setText(UiStrings().Transpose)
self.transpose_up_button.setText(UiStrings().Up)
self.transpose_down_button.setText(UiStrings().Down)

View File

@ -144,17 +144,8 @@ class VerseType(object):
names = ['Verse', 'Chorus', 'Bridge', 'Pre-Chorus', 'Intro', 'Ending', 'Other']
tags = [name[0].lower() for name in names]
translated_names = [
translate('SongsPlugin.VerseType', 'Verse'),
translate('SongsPlugin.VerseType', 'Chorus'),
translate('SongsPlugin.VerseType', 'Bridge'),
translate('SongsPlugin.VerseType', 'Pre-Chorus'),
translate('SongsPlugin.VerseType', 'Intro'),
translate('SongsPlugin.VerseType', 'Ending'),
translate('SongsPlugin.VerseType', 'Other')]
translated_tags = [name[0].lower() for name in translated_names]
translated_names = names
translated_tags = tags
@staticmethod
def translated_tag(verse_tag, default=Other):
@ -524,27 +515,30 @@ def strip_rtf(text, default_encoding=None):
return text, default_encoding
def delete_song(song_id, song_plugin):
def delete_song(song_id, trigger_event=True):
"""
Deletes a song from the database. Media files associated to the song are removed prior to the deletion of the song.
:param song_id: The ID of the song to delete.
:param song_plugin: The song plugin instance.
:param trigger_event: If True the song_deleted event is triggered through the registry
"""
save_path = ''
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
songs_manager = Registry().get('songs_manager')
media_files = songs_manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
for media_file in media_files:
try:
media_file.file_path.unlink()
except OSError:
log.exception('Could not remove file: {name}'.format(name=media_file.file_path))
try:
save_path = AppLocation.get_section_data_path(song_plugin.name) / 'audio' / str(song_id)
save_path = AppLocation.get_section_data_path('songs') / 'audio' / str(song_id)
if save_path.exists():
save_path.rmdir()
except OSError:
log.exception('Could not remove directory: {path}'.format(path=save_path))
song_plugin.manager.delete_object(Song, song_id)
songs_manager.delete_object(Song, song_id)
if trigger_event:
Registry().execute('song_deleted', song_id)
def transpose_lyrics(lyrics, transpose_value):

View File

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

View File

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

View File

@ -546,6 +546,7 @@ class SongMediaItem(MediaManagerItem):
new_song.media_files.append(new_media_file)
self.plugin.manager.save_object(new_song)
new_song.init_on_load()
Registry().execute('song_changed', new_song.id)
self.on_song_list_load()
def generate_slide_data(self, service_item, *, item=None, context=ServiceItemContext.Service, **kwargs):
@ -627,9 +628,11 @@ class SongMediaItem(MediaManagerItem):
if State().check_preconditions('media'):
service_item.add_capability(ItemCapabilities.HasBackgroundAudio)
total_length = 0
# We could have stored multiple files but only the first one will be played.
for m in song.media_files:
total_length += self.media_controller.media_length(m.file_path)
service_item.background_audio = [(m.file_path, m.file_hash) for m in song.media_files]
service_item.background_audio = [(m.file_path, m.file_hash)]
break
service_item.set_media_length(total_length)
service_item.metadata.append('<em>{label}:</em> {media}'.
format(label=translate('SongsPlugin.MediaItem', 'Media'),

View File

@ -229,7 +229,7 @@ class OpenLyrics(object):
self.manager = manager
FormattingTags.load_tags()
def song_to_xml(self, song):
def song_to_xml(self, song, version=None):
"""
Convert the song to OpenLyrics Format.
"""
@ -258,6 +258,9 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower())
if song.ccli_number:
self._add_text_to_element('ccliNo', properties, song.ccli_number)
# Add a custom version
if version:
self._add_text_to_element('version', properties, version)
if song.authors_songs:
authors = etree.SubElement(properties, 'authors')
for author_song in song.authors_songs:
@ -376,7 +379,7 @@ class OpenLyrics(object):
end_tags.reverse()
return ''.join(start_tags), ''.join(end_tags)
def xml_to_song(self, xml, parse_and_temporary_save=False):
def xml_to_song(self, xml, parse_and_temporary_save=False, update_song_id=None):
"""
Create and save a song from OpenLyrics format xml to the database. Since we also export XML from external
sources (e. g. OpenLyrics import), we cannot ensure, that it completely conforms to the OpenLyrics standard.
@ -398,7 +401,10 @@ class OpenLyrics(object):
# Formatting tags are new in OpenLyrics 0.8
if float(song_xml.get('version')) > 0.7:
self._process_formatting_tags(song_xml, parse_and_temporary_save)
song = Song()
if update_song_id:
song = self.manager.get_object(Song, update_song_id)
else:
song = Song()
# Values will be set when cleaning the song.
song.search_lyrics = ''
song.verse_order = ''

View File

@ -121,6 +121,7 @@ class SongsPlugin(Plugin):
"""
super(SongsPlugin, self).__init__('songs', SongMediaItem, SongsTab)
self.manager = DBManager('songs', init_schema, upgrade_mod=upgrade)
Registry().register('songs_manager', self.manager)
self.weight = -10
self.icon_path = UiIcons().music
self.icon = build_icon(self.icon_path)

View File

@ -69,6 +69,12 @@ def test_shortcuts(flask_client: FlaskClient, settings: Settings):
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

View File

@ -428,12 +428,26 @@ def test_sha256_file_hash_no_exist():
def test_sha256_file_hash_permission_error():
"""
Test SHA256 file hash when there is a permission error
Test that SHA256 file hash re-raises a permission error
"""
# GIVEN: A mocked Path object
mocked_path = MagicMock()
mocked_path.open.side_effect = PermissionError
# WHEN: Generating a hash for the file
# THEN: The PermissionError should be bubbled up
with pytest.raises(PermissionError):
sha256_file_hash(mocked_path)
def test_sha256_file_hash_other_error():
"""
Test SHA256 file hash when there is an error other than permission error
"""
# GIVEN: A mocked Path object
mocked_path = MagicMock()
mocked_path.open.side_effect = NotADirectoryError
# WHEN: Generating a hash for the file
result = sha256_file_hash(mocked_path)

View File

@ -30,7 +30,9 @@ from unittest.mock import MagicMock, patch
from PySide6 import QtCore, QtGui
from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \
get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb
get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, \
validate_thumb
from openlp.core.common.registry import Registry
from tests.utils.constants import RESOURCE_PATH
@ -275,7 +277,7 @@ def test_image_to_byte_base_64():
assert 'byte_array base64ified' == result, 'The result should be the return value of the mocked base64 method'
def test_create_thumb_with_size(registry):
def test_create_thumb_with_size(registry: Registry):
"""
Test the create_thumb() function with a given size.
"""
@ -310,7 +312,7 @@ def test_create_thumb_with_size(registry):
pass
def test_create_thumb_no_size(registry):
def test_create_thumb_no_size(registry: Registry):
"""
Test the create_thumb() function with no size specified.
"""
@ -345,7 +347,7 @@ def test_create_thumb_no_size(registry):
pass
def test_create_thumb_invalid_size(registry):
def test_create_thumb_invalid_size(registry: Registry):
"""
Test the create_thumb() function with invalid size specified.
"""
@ -381,7 +383,7 @@ def test_create_thumb_invalid_size(registry):
pass
def test_create_thumb_width_only(registry):
def test_create_thumb_width_only(registry: Registry):
"""
Test the create_thumb() function with a size of only width specified.
"""
@ -417,7 +419,7 @@ def test_create_thumb_width_only(registry):
pass
def test_create_thumb_height_only(registry):
def test_create_thumb_height_only(registry: Registry):
"""
Test the create_thumb() function with a size of only height specified.
"""
@ -453,7 +455,7 @@ def test_create_thumb_height_only(registry):
pass
def test_create_thumb_empty_img(registry):
def test_create_thumb_empty_img(registry: Registry):
"""
Test the create_thumb() function with a size of only height specified.
"""
@ -504,7 +506,7 @@ def test_create_thumb_empty_img(registry):
@patch('openlp.core.lib.QtGui.QImageReader')
@patch('openlp.core.lib.build_icon')
def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader, registry):
def test_create_thumb_path_fails(mocked_build_icon: MagicMock, MockQImageReader: MagicMock, registry: Registry):
"""
Test that build_icon() is run against the image_path when the thumbnail fails to be created
"""
@ -539,7 +541,7 @@ def test_check_item_selected_true():
assert result is True, 'The result should be True'
def test_check_item_selected_false(registry):
def test_check_item_selected_false(registry: Registry):
"""
Test that the check_item_selected() function returns False when there are no selected indexes.
"""
@ -610,7 +612,7 @@ def test_validate_thumb_file_exists_and_older():
assert result is False, 'The result should be False'
def test_resize_thumb(registry):
def test_resize_thumb(registry: Registry):
"""
Test the resize_thumb() function
"""
@ -632,7 +634,7 @@ def test_resize_thumb(registry):
assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'
def test_resize_thumb_ignoring_aspect_ratio(registry):
def test_resize_thumb_ignoring_aspect_ratio(registry: Registry):
"""
Test the resize_thumb() function ignoring aspect ratio
"""
@ -654,7 +656,7 @@ def test_resize_thumb_ignoring_aspect_ratio(registry):
assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'
def test_resize_thumb_width_aspect_ratio(registry):
def test_resize_thumb_width_aspect_ratio(registry: Registry):
"""
Test the resize_thumb() function using the image's width as the reference
"""
@ -672,7 +674,7 @@ def test_resize_thumb_width_aspect_ratio(registry):
assert wanted_width == result_size.width(), 'The image should have the requested width.'
def test_resize_thumb_same_aspect_ratio(registry):
def test_resize_thumb_same_aspect_ratio(registry: Registry):
"""
Test the resize_thumb() function when the image and the wanted aspect ratio are the same
"""
@ -691,7 +693,7 @@ def test_resize_thumb_same_aspect_ratio(registry):
@patch('openlp.core.lib.QtCore.QLocale.createSeparatedList')
def test_create_separated_list_qlocate(mocked_createSeparatedList):
def test_create_separated_list_qlocate(mocked_createSeparatedList: MagicMock):
"""
Test the create_separated_list function using the Qt provided method
"""

View File

@ -21,10 +21,10 @@
"""
Package to test the openlp.core.version package.
"""
import sys
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from requests.exceptions import ConnectionError
from openlp.core.version import VersionWorker, check_for_update, get_version, update_check_date
@ -251,15 +251,25 @@ def test_check_for_update_skipped(mocked_run_thread, mock_settings):
assert mocked_run_thread.call_count == 0
def test_get_version_dev_version():
@pytest.mark.parametrize('in_version, out_version', [
('3.1.1', {'full': '3.1.1', 'version': '3.1.1', 'build': None}),
('3.0.2+git.cb1db9f43', {'full': '3.0.2+git.cb1db9f43', 'version': '3.0.2', 'build': 'git.cb1db9f43'}),
('3.1.2.dev15+gff6b05ed3', {'full': '3.1.2.dev15+gff6b05ed3', 'version': '3.1.2', 'build': 'gff6b05ed3'})
])
@patch('openlp.core.version.AppLocation.get_directory')
def test_get_version(mocked_get_directory: MagicMock, in_version: str, out_version: dict):
"""
Test the get_version() function
"""
# GIVEN: We're in dev mode
with patch.object(sys, 'argv', ['--dev-version']), \
patch('openlp.core.version.APPLICATION_VERSION', None):
# WHEN: get_version() is run
# GIVEN: Some mocks and predefined versions
mocked_path = MagicMock()
mocked_path.__truediv__.return_value = mocked_path
mocked_path.read_text.return_value = in_version
mocked_get_directory.return_value = mocked_path
# WHEN: get_version() is run
with patch('openlp.core.version.APPLICATION_VERSION', None):
version = get_version()
# THEN: version is something
assert version
assert version == out_version

View File

@ -22,17 +22,20 @@
This module contains tests for the lib submodule of the Images plugin.
"""
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import ANY, MagicMock, patch
import pytest
from PySide6 import QtCore, QtWidgets
from openlp.core.common.registry import Registry
from openlp.core.common.enum import ImageThemeMode
from openlp.core.common.registry import Registry
from openlp.core.db.manager import DBManager
from openlp.core.lib import build_icon, create_thumb
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.widgets.views import TreeWidgetWithDnD
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
from tests.utils.constants import TEST_RESOURCES_PATH
@pytest.fixture
@ -258,3 +261,64 @@ def test_generate_thumbnail_path_filename(media_item):
# THEN: The path should be correct
assert result == Path('.') / 'myimage.jpg'
@patch('openlp.plugins.images.lib.mediaitem.create_thumb')
def test_load_item_file_not_exist(mocked_create_thumb: MagicMock, media_item: ImageMediaItem):
"""Test the load_item method when the file does not exist"""
# GIVEN: A media item and an Item to load
item = MagicMock(file_path=Path('myimage.jpg'), file_hash=None)
# WHEN load_item() is called with the Item
result = media_item.load_item(item)
# THEN: A QTreeWidgetItem with a "delete" icon should be returned
assert isinstance(result, QtWidgets.QTreeWidgetItem)
assert result.text(0) == 'myimage.jpg'
mocked_create_thumb.assert_not_called()
@patch('openlp.plugins.images.lib.mediaitem.validate_thumb')
@patch('openlp.plugins.images.lib.mediaitem.create_thumb', wraps=create_thumb)
@patch('openlp.plugins.images.lib.mediaitem.build_icon', wraps=build_icon)
def test_load_item_valid_thumbnail(mocked_build_icon: MagicMock, mocked_create_thumb: MagicMock,
mocked_validate_thumb: MagicMock, media_item: ImageMediaItem, registry: Registry):
"""Test the load_item method with an existing thumbnail"""
# GIVEN: A media item and an Item to load
media_item.service_path = Path(TEST_RESOURCES_PATH) / 'images'
mocked_validate_thumb.return_value = True
image_path = Path(TEST_RESOURCES_PATH) / 'images' / 'tractor.jpg'
item = MagicMock(file_path=image_path, file_hash=None)
# WHEN load_item() is called with the Item
result = media_item.load_item(item)
# THEN: A QTreeWidgetItem with a "delete" icon should be returned
assert isinstance(result, QtWidgets.QTreeWidgetItem)
assert result.text(0) == 'tractor.jpg'
assert result.toolTip(0) == str(image_path)
mocked_create_thumb.assert_not_called()
mocked_build_icon.assert_called_once_with(image_path)
@patch('openlp.plugins.images.lib.mediaitem.validate_thumb')
@patch('openlp.plugins.images.lib.mediaitem.create_thumb', wraps=create_thumb)
def test_load_item_missing_thumbnail(mocked_create_thumb: MagicMock, mocked_validate_thumb: MagicMock,
media_item: ImageMediaItem, registry: Registry):
"""Test the load_item method with no valid thumbnails"""
# GIVEN: A media item and an Item to load
with TemporaryDirectory() as tmpdir:
media_item.service_path = Path(tmpdir)
mocked_validate_thumb.return_value = False
image_path = Path(TEST_RESOURCES_PATH) / 'images' / 'tractor.jpg'
item = MagicMock(file_path=image_path, file_hash=None)
registry.get('settings').value.return_value = 400
# WHEN load_item() is called with the Item
result = media_item.load_item(item)
# THEN: A QTreeWidgetItem with a "delete" icon should be returned
assert isinstance(result, QtWidgets.QTreeWidgetItem)
assert result.text(0) == 'tractor.jpg'
assert result.toolTip(0) == str(image_path)
mocked_create_thumb.assert_called_once_with(image_path, Path(tmpdir, 'tractor.jpg'), size=QtCore.QSize(-1, 400))

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.