Compare commits

...

52 Commits

Author SHA1 Message Date
Raoul Snyman bfbd85deb4 Merge branch 'release-3.1.2' into 'master'
Release 3.1.2

See merge request openlp/openlp!752
2024-05-17 20:05:52 +00:00
Raoul Snyman 795aa22caa Merge branch 'update-changelog-3.1.2' into 'master'
Update the changelog for 3.1.2

See merge request openlp/openlp!751
2024-05-17 20:04:45 +00:00
Raoul Snyman 17240e2dd4 Update the changelog for 3.1.2 2024-05-17 20:04:45 +00:00
Raoul Snyman 912695dca9 Release 3.1.2 2024-05-15 22:20:01 -07:00
Raoul Snyman ca5304d91e Merge branch 'additional_planning_center_data' into 'master'
Additional planning center data

See merge request openlp/openlp!750
2024-05-14 16:03:45 +00:00
Tim Stephenson 2053029d7f Additional planning center data 2024-05-14 16:03:45 +00:00
Raoul Snyman dd466fb013 Merge branch 'uppercase-global' into 'master'
Add "Apply UPPERCASE globally" function to songs plugin.

See merge request openlp/openlp!749
2024-05-02 16:46:44 +00:00
Matey Krastev 45f68364a3 Add "Apply UPPERCASE globally" function to songs plugin. 2024-05-02 16:46:44 +00:00
Raoul Snyman 6bbfe00ec0 Merge branch 'translations-21042024' into 'master'
Translations 21042024

See merge request openlp/openlp!748
2024-04-27 05:42:58 +00:00
Tim Bentley cee0a9d573 Translations 21042024 2024-04-27 05:42:58 +00:00
Raoul Snyman 2ad33529e4 Merge branch 'issue-1877' into 'master'
Attempt to bubble up permissions errors to the user so that we don't run into None files or hashes

See merge request openlp/openlp!746
2024-04-20 22:26:55 +00:00
Raoul Snyman 7bb7dc35c0 Merge branch 'minor_fixes2' into 'master'
Stop Service File items from containing more than one audio file

See merge request openlp/openlp!742
2024-04-20 22:14:30 +00:00
Tim Bentley 9b9d8feafa Stop Service File items from containing more than one audio file 2024-04-20 22:14:29 +00:00
Raoul Snyman 32d132c2f0 Merge branch 'issue-1883' into 'master'
Fix build part of version number

Closes #1883

See merge request openlp/openlp!745
2024-04-20 22:13:26 +00:00
Raoul Snyman f5e0682e0d Fix build part of version number 2024-04-20 22:13:26 +00:00
Raoul Snyman 7d44795cf7 Merge branch 'issue-1878' into 'master'
Attempt to fix #1878 by checking if the service item exists first

Closes #1878

See merge request openlp/openlp!744
2024-04-20 22:13:08 +00:00
Raoul Snyman 64f1a0e52d Attempt to bubble up permissions errors to the user so that we don't run into None files or hashes 2024-04-19 23:11:01 -07:00
Raoul Snyman d2a2b94273 Merge branch 'hide-live-when-screen-setup-has-changed' into 'master'
Hide live when screen setup has changed

See merge request openlp/openlp!743
2024-04-20 05:42:23 +00:00
Chris Witterholt e93ac46f2a Hide live when screen setup has changed 2024-04-20 05:42:23 +00:00
Raoul Snyman 2639c7cd00 Attempt to fix #1878 by checking if the service item exists first 2024-04-19 21:59:07 -07:00
Tomas Groth 6d636f3235 Merge branch 'plugin-hooks' into 'master'
Add some registry functions and more that makes it easier for plugins to integrate

See merge request openlp/openlp!741
2024-04-17 20:06:18 +00:00
Tomas Groth b8855eb8ed Add some registry functions and more that makes it easier for plugins to integrate 2024-04-17 17:14:58 +02:00
Tim Bentley d1724bc6c3 Merge branch 'issue-1880' into 'master'
Make the slide height affect the size of the thumbnails generated

Closes #1880

See merge request openlp/openlp!738
2024-04-16 20:58:55 +00:00
Tim Bentley 62d6b61518 Merge branch 'fix-for-not-found-i18n-directory' into 'master'
Fix for not found i18n directory

See merge request openlp/openlp!739
2024-04-16 18:19:45 +00:00
Chris Witterholt 9b794e4ff8 Fix for not found i18n directory 2024-04-16 18:19:45 +00:00
Tim Bentley 6cfa2419e3 Merge branch 'fix-missing-verse-translations' into 'master'
Fix missing verse translations

See merge request openlp/openlp!740
2024-04-16 18:16:24 +00:00
Chris Witterholt 027391d321 Fix missing verse translations 2024-04-16 18:16:24 +00:00
Raoul Snyman 885e57ba41 Make the slide height affect the size of the thumbnails generated 2024-04-14 22:36:19 -07:00
Tomas Groth 6498b66698 Merge branch 'import-ewsx' into 'master'
Add ewsx song importer

See merge request openlp/openlp!737
2024-04-09 15:19:58 +00:00
Tomas Groth fa05ee4b2d Add ewsx song importer 2024-04-09 15:19:58 +00:00
Raoul Snyman 39833c770d Merge branch 'add-webapi-endpoint-get-translation' into 'master'
Add web API endpoint get configured language

See merge request openlp/openlp!730
2024-03-28 03:17:33 +00:00
Chris Witterholt fd45bba951 Add web API endpoint get configured language 2024-03-28 03:17:33 +00:00
Raoul Snyman f2484d170a Merge branch 'add-webapi-endpoint-get-shortcuts' into 'master'
Add web API endpoint get configured shortcut keys.

See merge request openlp/openlp!734
2024-03-27 19:12:22 +00:00
Chris Witterholt 34cc2a3e28 Add web API endpoint get configured shortcut keys. 2024-03-27 19:12:22 +00:00
Tim Bentley b569a1793d Merge branch 'bug-940' into 'master'
Add checks to prevent multiple Linked Audio items on songs

Closes #940

See merge request openlp/openlp!728
2024-03-25 07:51:34 +00:00
Trildar f3c675901c Add checks to prevent multiple Linked Audio items on songs 2024-03-25 07:51:34 +00:00
Raoul Snyman 34ff2dab8b Merge branch 'fix-1871' into 'master'
Further fix #1871 by adding the Application name as early as possible

Closes #1871

See merge request openlp/openlp!733
2024-03-24 19:59:36 +00:00
Raoul Snyman 78c32e434c Further fix #1871 by adding the Application name as early as possible 2024-03-24 10:43:29 -07:00
Tomas Groth 1a281e3931 Merge branch 'issue-1871' into 'master'
Fix unintentional change of the organization name by the domain name.

Closes #1871

See merge request openlp/openlp!732
2024-03-23 20:50:01 +00:00
Chris Witterholt 490b25b73b Fix unintentional change of the organization name by the domain name. 2024-03-23 20:50:00 +00:00
Raoul Snyman cb1db9f432 Merge branch 'fix-missing-translations' into 'master'
Fix missing translations

See merge request openlp/openlp!729
2024-03-18 06:13:26 +00:00
Chris Witterholt 6ea889b974 Fix missing translations 2024-03-18 06:13:26 +00:00
Raoul Snyman a53e20864c Merge branch 'release-3.1.1' into 'master'
Release 3.1.1

See merge request openlp/openlp!727
2024-03-10 00:57:32 +00:00
Raoul Snyman e5f6850d14 Release 3.1.1 2024-03-08 21:09:01 -07:00
Tomas Groth 87deeb58cc Merge branch 'issue-1860' into 'master'
Fix path to QtWebEngineProcess binary

Closes #1860

See merge request openlp/openlp!726
2024-03-07 05:41:43 +00:00
Raoul Snyman 2f54765670 Fix path to QtWebEngineProcess binary 2024-03-06 22:05:34 -07:00
Raoul Snyman 3c20d5f8d6 Merge branch 'songselect-import-encoding' into 'master'
Always open downloaded songs as utf-8

See merge request openlp/openlp!725
2024-03-07 03:56:37 +00:00
Raoul Snyman a40d5e894e Merge branch 'issue-1859' into 'master'
Use Python's version comparison, not Qt's

Closes #1859

See merge request openlp/openlp!724
2024-03-06 15:58:22 +00:00
Raoul Snyman 5508bb683c Use Python's version comparison, not Qt's 2024-03-06 15:58:22 +00:00
Tomas Groth cb490b9e59 Always open downloaded songs as utf-8 2024-03-06 13:08:02 +00:00
Tim Bentley 0d3acc2e67 Merge branch 'translations-01032024aa' into 'master'
Translations 01032024aa

See merge request openlp/openlp!723
2024-03-01 11:34:07 +00:00
Tim Bentley 9747f16de9 Translations 01032024aa 2024-03-01 11:34:07 +00:00
96 changed files with 14654 additions and 12817 deletions

View File

@ -1,3 +1,177 @@
OpenLP 3.1.2
============
* Import additional planning center data
* Add "Apply UPPERCASE globally" function to songs plugin
* Update translations
* Stop Service File items from containing more than one audio file
* Fix build part of version number
* Attempt to bubble up permissions errors to the user so that we don't run into None files or hashes
* Hide live when screen setup has changed
* Attempt to fix #1878 by checking if the service item exists first
* Add some registry functions and more that makes it easier for plugins to integrate
* Fix for not found i18n directory
* Fix missing verse translations
* Make the slide height affect the size of the thumbnails generated
* Add ewsx song importer
* Add web API endpoint get configured language
* Add web API endpoint get configured shortcut keys
* Add checks to prevent multiple Linked Audio items on songs
* Set up the Application name as early as possible
* Fix unintentional change of the organization name by the domain name.
* Fix missing translations
OpenLP 3.1.1
============
* Fix path to QtWebEngineProcess binary
* Use Python's version comparison, not Qt's
* Always open downloaded songs as utf-8
* Update translations
OpenLP 3.1.0
============
* Change bug reporting email address to differentiate between affected versions
* Update translations
* Set the app's desktop file name
* tests: add ``assert_`` prefix to a bunch of asserts missing it
* Invalidate the service item cache when the theme changes
* Replace appdirs with platformdirs
* Fix a PermissionError that occurs on Windows 10/11 when qtawesome tries to look at its own fonts
* Change the filter to be SQLAlchemy 2 compatible
* Working version of Community Imports
* Fix #1323 for the Projector Manager
* Made the wordproject import more robust
OpenLP 3.1.0rc4
===============
* Fix a loop in the First Time Wizard on Windows
* Fix portable builds by re-arranging when the settings are created
OpenLP 3.1.0rc3
===============
* Fix the coverage badge on GitLab by producing an XML report
* Fix irregular service theme saving (closes #1723)
* Fix AuthorType not getting translated
* Fix bug in _has_header
* Fix issues with upgrading 2.9.x databases
* Update translations
* Fix OpenLP startup by reordering statements
* High DPI Fixes
* Fix traceback on bible import when no bible available
* Check before initialising a None Bible
* Fix #1700 by typecasting the calls to Paths
* Make PathEdit handle None values
* Fix external DB settings
* Fix alerts
* Fix handling of missing VLC
* Better handling of attempts to load invalid SWORD folder or zip-file
* Ensure a path set in PathEdit is a Path instance
* Fix trimming leading whitespaces
* Inject String.replaceAll javascript implementation if needed into webengine when browsing SongSelect.
* Do not start the same presentation again when it's already live.
* Prevent key error when unblank screen at start of presentation.
OpenLP 3.1.0rc2
===============
* Revert the Registry behaviour
* Fix the multiselect in the images plugin
* Spoof the songselect webengine user agent
OpenLP 3.1.0rc1
===============
* Don't build manual, use online manual instead
* Update AppVeyor for Mac to install Pyro5 instead of Pyro4
* Silence error when shutting down threads
* Fix saving of songs
* Update some system messaging
* Re introduce the selective turning off logging - correctly this time.
* Fix some issues with building on macOS
* Fix spelling in songimport.py
* Bypass image db updates if the db has already been upgraded
* Fix a couple of macOS issues
* Fix issue with database cleanup code
* Make some forward compatibility changes
* Refactor last instances of TestCase-based tests
* Change SongSelect import procedure to import when clicking download on webpage
* Add test coverage for __main__.py and remove some unused files
* Remove unused flag in Registry
* When a permission error is raised during generation of the sha256 hash when deleting a presentation from the controller don't crash but continue.
* Fix presentations not being able to return from Display Screen
* fix the deadlock on macos
* Fix issue #1618 by ignoring the messages if the event loop is not running
* Fix issue #1382 by waiting for the service_manager to become available, or giving up after 2m
* Display API abstraction
* Try to fix an issue with MediaInfo perhaps returning a str instead of an int
* Fix issue #1582 by running the search in the original thread
* Try to fix an issue that only seems to happen on macOS
* Allow loading the same presentation file multiple times from 2.4.x service file. Fixes bug #1601.
* Fix endless loop at the end of a PowerPoint presentation
* Implement a filelock for shared data folder.
* Add detection for presentation files that were uploaded from the cloud.
* Move "Live" / "Preview" and current item on one line
* feat(importer): add authors to powerpraise importer
* Add the list of associated songs to the delete dialog in the song maintenance form
* Create a connection and then run execute
* Update appveyor.yml to use python 3.11.
* Fix an issue with the arguments of with_only_columns
* Fix song search by author
* Remove dependency on PIL since the latest version does not support PyQt5
* Fixing freezing screenshot test
* Fix Datasoul translate strings
* RFC/Proposal: Fallback code for display screenshot code (used on '/main' Web Remote)
* Update translations
* New theme adjustments: Adding letter spacing to theme main area; adding line and letter spacing to footer
* Fix the GitLab CI yaml config
* Fix issue #1297 by reducing the number by 1024 times
* Update resource generation for ARM64 platforms (e.g. Apple M2)
* Enumm Conversion
* Upgrade to Pyro5
* Ignore the thumbnails if the path doesn't exist (fixes #914)
* Adding Footer Content as Extra First Slide
* Fix an issue where an item's parent is None
* Migrate to SQLAlchemy 2 style queries
* Fix the 415 errors due to a change in Werkzeug
* Update CI to use the GitLab container registry
* Display Custom Scheme
* Implementing new message websocket endpoint
* Fix bug in icon definition - Typr only
* Take account of VLC on macOS being bundled with OpenLP
* Fix for #1495 task: wrapped C/C++ object of type QTreeWidgetItem has been deleted
* Fixing Images not being able to be inserted on Service
* Reusable Media Toolbar
* Adding foundational support to Footer per slide
* Merge CustomXMLBuilder and CustomXMLParser
* Add Datasoul song importer
* fix: tests on windows failing due to MagicMock in Path
* Migrate from FontAwesome4 to Material Design Icons v5.9.55
* Highlighted slidecontroller buttons
* Fix translations loading on linux system-wide installation
* Migrate database metadata to declarative base
* Migrate Song Usage to declarative
* Migrate alerts to declarative
* Migrate Images plugin to use shared folder code
* Fix a typo in creating custom slides from other text items
* Migrate images plugin to declarative base
* Convert Bibles to use declarative_base
* Convert custom slides to declarative
* Migrate to using Declarative Base in Songs
* Fix: Correct About references and Remove Unused
* Minor fix for EasyWorship import
* Improve Powerpoint detection by trying to start the application instead of looking it up in the registry.
* Fix selected=True not being set at new Transpose API Endpoint
* Allow the remote interface update notification to be turned off.
* Skip missing thumbnails when loading a service
* Rework the songs settings, so that they're not as squashed.
* Remove WebOb -- we don't need it
* Add a grid view to themes manager
OpenLP 3.0.2
============
@ -19,7 +193,6 @@ OpenLP 3.0.2
* Fix an issue where the websockets server would try to shut down even when -w is supplied
* Use a simpler approach when creating a tmp file when saving service files
OpenLP 2.5.1
============

View File

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

View File

@ -1 +1 @@
3.1.0
3.1.2

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

View File

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

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

@ -363,6 +363,7 @@ class Settings(QtCore.QSettings):
'songs/chord notation': 'english', # Can be english, german or neo-latin
'songs/disable chords import': False,
'songs/auto play audio': False,
'songs/uppercase songs': False,
'songusage/status': PluginStatus.Inactive,
'songusage/db type': 'sqlite',
'songusage/db username': '',

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

@ -1035,11 +1035,10 @@ 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.'),
self.live_controller.toggle_display('desktop')
QtWidgets.QMessageBox.information(self,
UiStrings().ScreenSetupHasChangedTitle,
UiStrings().ScreenSetupHasChanged,
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
self.screen_change_timestamp = datetime.now()
self.application.set_busy_cursor()

View File

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

@ -1265,8 +1265,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

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

@ -119,6 +119,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.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 PyQt5 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.UserRole, item)
tree_item.setIcon(0, icon)

View File

@ -198,6 +198,16 @@ class SelectPlanForm(QtWidgets.QDialog, Ui_SelectPlanDialog):
if len(song_data) and len(arrangement_data):
break
author = song_data['attributes']['author']
try:
copyright = song_data['attributes']['copyright']
except KeyError:
log.error("no copyright info for %s", item_title)
copyright = ""
try:
ccli_no = song_data['attributes']['ccli_number']
except KeyError:
log.error("no ccli_number info for %s", item_title)
ccli_no = ""
lyrics = arrangement_data['attributes']['lyrics']
arrangement_updated_at = datetime.strptime(arrangement_data['attributes']['updated_at'].
rstrip("Z"), '%Y-%m-%dT%H:%M:%S')
@ -205,7 +215,8 @@ class SelectPlanForm(QtWidgets.QDialog, Ui_SelectPlanDialog):
planning_center_import = PlanningCenterSongImport()
theme_name = self.song_theme_selection_combo_box.currentText()
openlp_id = planning_center_import.add_song(item_title, author, lyrics,
theme_name, arrangement_updated_at)
theme_name, arrangement_updated_at,
copyright, ccli_no)
planning_center_id_to_openlp_id[song_id] = openlp_id
openlp_id = planning_center_id_to_openlp_id[song_id]
media_type = 'songs'

View File

@ -44,7 +44,7 @@ class PlanningCenterSongImport(SongImport):
manager = songs.plugin.manager
SongImport.__init__(self, manager, file_path=None)
def add_song(self, item_title, author, lyrics, theme_name, last_modified):
def add_song(self, item_title, author, lyrics, theme_name, last_modified, copyright="", ccli_number=""):
"""
Builds and adds song to the database and returns the Song ID
:param item_title: The song title.
@ -52,12 +52,16 @@ class PlanningCenterSongImport(SongImport):
:param lyrics: Lyrics String from Planning Center
:param theme_name: Theme String to use for this song
:param last_modified: DateTime of last modified date for this song
:param copyright: Copyright statement for this song
:param ccli_no: CCLI number for this song
"""
self.set_defaults()
self.title = item_title
self.theme_name = theme_name
if author:
self.parse_author(author)
self.ccli_number = ccli_number
self.copyright = copyright
# handle edge condition where a song has no lyrics set
if lyrics is None:
self.add_verse(item_title)

View File

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

View File

@ -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)
@ -1114,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 PyQt5 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

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

View File

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

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):
@ -384,7 +375,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='')
@ -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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
@ -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):
@ -559,6 +560,7 @@ class SongMediaItem(MediaManagerItem):
"""
log.debug('generate_slide_data: {service}, {item}, {remote}'.format(service=service_item, item=item,
remote=self.remote_song))
uppercase = bool(self.settings.value('songs/uppercase songs'))
item_id = self._get_id_of_item_to_generate(item, self.remote_song)
service_item.add_capability(ItemCapabilities.CanEdit)
service_item.add_capability(ItemCapabilities.CanPreview)
@ -596,6 +598,8 @@ class SongMediaItem(MediaManagerItem):
verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
force_verse = verse[1].split('[--}{--]\n')
for split_verse in force_verse:
if uppercase:
split_verse = "{uc}" + split_verse + "{/uc}"
service_item.add_from_text(split_verse, verse_def)
else:
# Loop through the verse list and expand the song accordingly.
@ -613,6 +617,8 @@ class SongMediaItem(MediaManagerItem):
verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
force_verse = verse[1].split('[--}{--]\n')
for split_verse in force_verse:
if uppercase:
split_verse = "{uc}" + split_verse + "{/uc}"
service_item.add_from_text(split_verse, verse_def)
service_item.data_string = {
'title': song.search_title,
@ -627,9 +633,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'),
@ -673,7 +681,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))

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

@ -54,6 +54,9 @@ class SongsTab(SettingsTab):
self.auto_play_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.auto_play_check_box.setObjectName('auto_play_check_box')
self.mode_layout.addWidget(self.auto_play_check_box)
self.uppercase_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.uppercase_check_box.setObjectName('uppercase_check_box')
self.mode_layout.addWidget(self.uppercase_check_box)
# First Slide Mode
self.first_slide_mode_widget = QtWidgets.QWidget(self.mode_group_box)
self.first_slide_mode_layout = QtWidgets.QHBoxLayout(self.first_slide_mode_widget)
@ -152,6 +155,7 @@ class SongsTab(SettingsTab):
self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
self.first_slide_mode_combobox.currentIndexChanged.connect(self.on_first_slide_mode_combo_box_changed)
self.auto_play_check_box.stateChanged.connect(self.on_auto_play_check_box_changed)
self.uppercase_check_box.stateChanged.connect(self.on_uppercase_check_box_changed)
self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
self.song_key_warning_check_box.stateChanged.connect(self.on_song_key_warning_check_box_changed)
self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
@ -171,6 +175,7 @@ class SongsTab(SettingsTab):
self.first_slide_mode_combobox.setItemText(1, translate('SongsPlugin.SongsTab', 'Songbook'))
self.first_slide_mode_combobox.setItemText(2, translate('SongsPlugin.SongsTab', 'Same as Footer'))
self.auto_play_check_box.setText(translate('SongsPlugin.SongsTab', 'Auto-play background audio'))
self.uppercase_check_box.setText(translate('SongsPlugin.SongsTab', 'Apply UPPERCASE globally to all songs.'))
self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
'be regarded as chords.'))
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
@ -253,6 +258,9 @@ class SongsTab(SettingsTab):
def on_auto_play_check_box_changed(self, check_state):
self.auto_play = (check_state == QtCore.Qt.Checked)
def on_uppercase_check_box_changed(self, check_state):
self.uppercase = (check_state == QtCore.Qt.Checked)
def on_disable_chords_import_check_box_changed(self, check_state):
self.disable_chords_import = (check_state == QtCore.Qt.Checked)
@ -280,6 +288,7 @@ class SongsTab(SettingsTab):
self.update_load = self.settings.value('songs/add song from service')
self.first_slide_mode = self.settings.value('songs/first slide mode')
self.auto_play = self.settings.value('songs/auto play audio')
self.uppercase = self.settings.value('songs/uppercase songs')
self.enable_chords = self.settings.value('songs/enable chords')
self.chord_notation = self.settings.value('songs/chord notation')
self.disable_chords_import = self.settings.value('songs/disable chords import')
@ -288,6 +297,7 @@ class SongsTab(SettingsTab):
self.update_on_edit_check_box.setChecked(self.update_edit)
self.add_from_service_check_box.setChecked(self.update_load)
self.auto_play_check_box.setChecked(self.auto_play)
self.uppercase_check_box.setChecked(self.uppercase)
self.chords_group_box.setChecked(self.enable_chords)
self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
self.song_key_warning_check_box.setChecked(self.song_key_warning)
@ -311,6 +321,7 @@ class SongsTab(SettingsTab):
self.settings.setValue('songs/update service on edit', self.update_edit)
self.settings.setValue('songs/add song from service', self.update_load)
self.settings.setValue('songs/auto play audio', self.auto_play)
self.settings.setValue('songs/uppercase songs', self.uppercase)
self.settings.setValue('songs/enable chords', self.chords_group_box.isChecked())
self.settings.setValue('songs/disable chords import', self.disable_chords_import)
self.settings.setValue('songs/warn about missing song key', self.song_key_warning)

View File

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

100
scripts/check-i18n.py Executable file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env python
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2024 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import sys
import xml.etree.ElementTree as ET
from argparse import ArgumentParser, Namespace
from pathlib import Path
def get_args() -> Namespace:
"""Get the command line arguments"""
parser = ArgumentParser()
parser.add_argument('-b', '--base-path', help='Base path containing all the translation files', required=True)
return parser.parse_args()
def find_variables(text: str) -> set[str]:
"""Find the variables in a string"""
variables = set()
remaining_text = text
start_idx = remaining_text.find('{')
while start_idx >= 0:
end_idx = remaining_text.find('}')
variables.add(remaining_text[start_idx:end_idx + 1])
remaining_text = remaining_text[end_idx + 1:]
start_idx = remaining_text.find('{')
return variables
def check_for_mismatching_variables(xml_file: Path) -> list[dict[str, str]]:
"""Load an XML file and check it for mismatched variables, returning any errors"""
errors: list[dict[str, str]] = []
tree = ET.parse(xml_file)
root = tree.getroot()
messages = root.findall('.//context/message')
for message in messages:
source = message.find('source').text.strip() # type: ignore[union-attr]
translation = message.find('translation').text # type: ignore[union-attr]
if translation is None:
continue
else:
translation = translation.strip()
if '{' not in source or source == '{ And }':
continue
# Find text between "{" and "}" in source
# set(source[source.find('{')+1:source.find('}')].split())
source_variables = find_variables(source)
# Find text between "{" and "}" in translation
# set(translation[translation.find('{')+1:translation.find('}')].split())
translation_variables = find_variables(translation)
# Check if the same text exists in both source and translation
if source_variables != translation_variables:
errors.append({'source': source, 'translation': translation})
return errors
def main():
"""Run through all the i18n files and check that the variables in the translation match the source"""
exit_code = 0
args = get_args()
xml_files = Path(args.base_path).glob('*.ts')
for ts_file in xml_files:
errors = check_for_mismatching_variables(ts_file)
if errors:
exit_code = 1
print('=========================================')
print('Found errors in %s' % ts_file)
for error in errors:
print('>', error['source'])
print('<', error['translation'])
return exit_code
if __name__ == "__main__":
sys.exit(main())

View File

@ -95,6 +95,7 @@ MODULES = [
'pymediainfo',
'vlc',
'qrcode',
'packaging',
]

View File

@ -42,4 +42,11 @@ echo Downloading the translated files
echo
tx pull -a --minimum-perc=45
echo Checking for tag errors
./check-i18n.py -b ../resources/i18n/
echo Translation update complete

View File

@ -105,6 +105,7 @@ using a computer and a display/projector.""",
'flask-cors',
'lxml',
'Mako',
'packaging',
'platformdirs',
'PyICU',
'pymediainfo >= 2.2',

View File

@ -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'] == '*'

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

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

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

@ -66,13 +66,17 @@ def test_add_song_with_lyrics(MockAuthor: MagicMock, MockSong: MagicMock, song_i
# GIVEN: A PlanningCenterSongImport Class and some values
item_title = 'Title'
author = 'Author'
copyright = "Copyright"
ccli_number = 1111
lyrics = 'This is my song!'
theme_name = 'Theme Name'
last_modified = datetime.datetime.now()
# WHEN: A song is added with lyrics
song_import.add_song(item_title, author, lyrics, theme_name, last_modified)
song_import.add_song(item_title, author, lyrics, theme_name, last_modified, copyright, ccli_number)
# THEN: A mock song has valid title, lyrics, and theme_name values
assert MockSong.return_value.title == item_title, 'Mock Song Title matches input title'
assert MockSong.return_value.copyright == copyright
assert MockSong.return_value.ccli_number == ccli_number
assert lyrics in MockSong.return_value.lyrics, 'Mock Song Lyrics contain input lyrics'
assert MockSong.return_value.theme_name == theme_name, 'Mock Song Theme matches input theme'
@ -86,13 +90,17 @@ def test_add_song_with_verse(MockAuthor: MagicMock, MockSong: MagicMock, song_im
# GIVEN: A PlanningCenterSongImport Class
item_title = 'Title'
author = 'Author'
copyright = "Copyright"
ccli_number = 1111
lyrics = 'V1\nThis is my song!'
theme_name = 'Theme Name'
last_modified = datetime.datetime.now()
# WHEN: A song is added with lyrics that contain a verse tag
song_import.add_song(item_title, author, lyrics, theme_name, last_modified)
song_import.add_song(item_title, author, lyrics, theme_name, last_modified, copyright, ccli_number)
# THEN: A mock song has valid title, lyrics, and theme_name values
assert MockSong.return_value.title == item_title, 'Mock Song Title matches input title'
assert MockSong.return_value.copyright == copyright
assert MockSong.return_value.ccli_number == ccli_number
assert 'This is my song!' in MockSong.return_value.lyrics, 'Mock Song Lyrics contain input lyrics'
assert 'type="v"' in MockSong.return_value.lyrics, 'Mock Song Lyrics contain input verse'
assert MockSong.return_value.theme_name == theme_name, 'Mock Song Theme matches input theme'

View File

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

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

View File

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

View File

@ -92,6 +92,7 @@ def test_save_check_box_settings(form):
form.on_add_from_service_check_box_changed(QtCore.Qt.Checked)
form.on_disable_chords_import_check_box_changed(QtCore.Qt.Unchecked)
form.on_auto_play_check_box_changed(QtCore.Qt.Checked)
form.on_uppercase_check_box_changed(QtCore.Qt.Checked)
# WHEN: Save is invoked
form.save()
# THEN: The correct values should be stored in the settings
@ -101,6 +102,7 @@ def test_save_check_box_settings(form):
assert form.settings.value('songs/add song from service') is True
assert form.settings.value('songs/disable chords import') is False
assert form.settings.value('songs/auto play audio') is True
assert form.settings.value('songs/uppercase songs') is True
def test_english_notation_button(form):
@ -149,7 +151,7 @@ def test_password_change(mocked_settings_set_val, mocked_question, form):
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once()
assert mocked_settings_set_val.call_count == 11
assert mocked_settings_set_val.call_count == 12
@patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question')
@ -165,7 +167,7 @@ def test_password_change_cancelled(mocked_settings_set_val, mocked_question, for
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once()
assert mocked_settings_set_val.call_count == 10
assert mocked_settings_set_val.call_count == 11
@patch('openlp.core.common.settings.Settings.setValue')
@ -177,7 +179,7 @@ def test_footer_nochange(mocked_settings_set_val, form):
# WHEN: save is invoked
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
assert mocked_settings_set_val.call_count == 11
assert mocked_settings_set_val.call_count == 12
@patch('openlp.core.common.settings.Settings.setValue')
@ -190,7 +192,7 @@ def test_footer_change(mocked_settings_set_val, form):
# WHEN: save is invoked
form.save()
# THEN: footer should have been saved (one more call to setValue than the nochange test)
assert mocked_settings_set_val.call_count == 12
assert mocked_settings_set_val.call_count == 13
assert form.footer_edit_box.toPlainText() == 'A new footer'

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.