mirror of https://gitlab.com/openlp/openlp.git
Compare commits
52 Commits
Author | SHA1 | Date |
---|---|---|
Raoul Snyman | bfbd85deb4 | |
Raoul Snyman | 795aa22caa | |
Raoul Snyman | 17240e2dd4 | |
Raoul Snyman | 912695dca9 | |
Raoul Snyman | ca5304d91e | |
Tim Stephenson | 2053029d7f | |
Raoul Snyman | dd466fb013 | |
Matey Krastev | 45f68364a3 | |
Raoul Snyman | 6bbfe00ec0 | |
Tim Bentley | cee0a9d573 | |
Raoul Snyman | 2ad33529e4 | |
Raoul Snyman | 7bb7dc35c0 | |
Tim Bentley | 9b9d8feafa | |
Raoul Snyman | 32d132c2f0 | |
Raoul Snyman | f5e0682e0d | |
Raoul Snyman | 7d44795cf7 | |
Raoul Snyman | 64f1a0e52d | |
Raoul Snyman | d2a2b94273 | |
Chris Witterholt | e93ac46f2a | |
Raoul Snyman | 2639c7cd00 | |
Tomas Groth | 6d636f3235 | |
Tomas Groth | b8855eb8ed | |
Tim Bentley | d1724bc6c3 | |
Tim Bentley | 62d6b61518 | |
Chris Witterholt | 9b794e4ff8 | |
Tim Bentley | 6cfa2419e3 | |
Chris Witterholt | 027391d321 | |
Raoul Snyman | 885e57ba41 | |
Tomas Groth | 6498b66698 | |
Tomas Groth | fa05ee4b2d | |
Raoul Snyman | 39833c770d | |
Chris Witterholt | fd45bba951 | |
Raoul Snyman | f2484d170a | |
Chris Witterholt | 34cc2a3e28 | |
Tim Bentley | b569a1793d | |
Trildar | f3c675901c | |
Raoul Snyman | 34ff2dab8b | |
Raoul Snyman | 78c32e434c | |
Tomas Groth | 1a281e3931 | |
Chris Witterholt | 490b25b73b | |
Raoul Snyman | cb1db9f432 | |
Chris Witterholt | 6ea889b974 | |
Raoul Snyman | a53e20864c | |
Raoul Snyman | e5f6850d14 | |
Tomas Groth | 87deeb58cc | |
Raoul Snyman | 2f54765670 | |
Raoul Snyman | 3c20d5f8d6 | |
Raoul Snyman | a40d5e894e | |
Raoul Snyman | 5508bb683c | |
Tomas Groth | cb490b9e59 | |
Tim Bentley | 0d3acc2e67 | |
Tim Bentley | 9747f16de9 |
175
CHANGELOG.rst
175
CHANGELOG.rst
|
@ -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
|
||||
============
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ install:
|
|||
# Update pip
|
||||
- python -m pip install --upgrade pip
|
||||
# Install generic dependencies from pypi.
|
||||
- python -m pip install sqlalchemy alembic platformdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc flask-cors pytest-qt pyenchant pysword qrcode flask
|
||||
- python -m pip install sqlalchemy alembic platformdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock psycopg2-binary websockets waitress six requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF QDarkStyle python-vlc flask-cors pytest-qt pyenchant pysword qrcode flask packaging
|
||||
# Install Windows only dependencies
|
||||
- cmd: python -m pip install pyodbc pypiwin32
|
||||
- cmd: choco install vlc %CHOCO_VLC_ARG% --no-progress --limit-output
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.1.0
|
||||
3.1.2
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -24,6 +24,7 @@ from flask import jsonify, request, abort, Blueprint
|
|||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.api.lib import login_required
|
||||
from openlp.core.common.i18n import LanguageManager
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib.plugin import PluginStatus, StringContent
|
||||
from openlp.core.state import State
|
||||
|
@ -56,16 +57,38 @@ def plugin_list():
|
|||
return jsonify(searches)
|
||||
|
||||
|
||||
@core.route('/shortcuts')
|
||||
def shortcuts():
|
||||
data = []
|
||||
settings = Registry().get('settings_thread')
|
||||
shortcut_prefix = 'shortcuts/'
|
||||
for key in settings.allKeys():
|
||||
if key.startswith(shortcut_prefix):
|
||||
data.append(
|
||||
{
|
||||
'action': key.removeprefix(shortcut_prefix),
|
||||
'shortcut': settings.value(key)
|
||||
}
|
||||
)
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@core.route('/system')
|
||||
def system_information():
|
||||
data = {}
|
||||
data['websocket_port'] = Registry().get('settings_thread').value('api/websocket port')
|
||||
data['login_required'] = Registry().get('settings_thread').value('api/authentication enabled')
|
||||
data['api_version'] = 2
|
||||
data['api_revision'] = 4
|
||||
data['api_revision'] = 5
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@core.route('/language')
|
||||
def language():
|
||||
language = LanguageManager.get_language()
|
||||
return jsonify({'language': language})
|
||||
|
||||
|
||||
@core.route('/login', methods=['POST'])
|
||||
def login():
|
||||
data = request.json
|
||||
|
|
|
@ -485,12 +485,12 @@ def main():
|
|||
# support dark mode on windows 10. This makes the titlebar dark, the rest is setup later
|
||||
# by calling set_windows_darkmode
|
||||
qt_args.extend(['-platform', 'windows:darkmode=1'])
|
||||
elif is_macosx() and getattr(sys, 'frozen', False) and not os.environ.get('QTWEBENGINEPROCESS_PATH'):
|
||||
# Work around an issue where PyInstaller is not setting this environment variable
|
||||
os.environ['QTWEBENGINEPROCESS_PATH'] = str(AppLocation.get_directory(AppLocation.AppDir) / 'PyQt5' / 'Qt5' /
|
||||
'lib' / 'QtWebEngineCore.framework' / 'Versions' / '5' /
|
||||
'Helpers' / 'QtWebEngineProcess.app' / 'Contents' / 'MacOS' /
|
||||
'QtWebEngineProcess')
|
||||
elif is_macosx() and getattr(sys, 'frozen', False):
|
||||
# Set the location to the QtWebEngineProcess binary, normally set by PyInstaller, but it moves around...
|
||||
os.environ['QTWEBENGINEPROCESS_PATH'] = str((AppLocation.get_directory(AppLocation.AppDir) / '..' /
|
||||
'Frameworks' / 'QtWebEngineCore.framework' / 'Versions' / '5' /
|
||||
'Helpers' / 'QtWebEngineProcess.app' / 'Contents' / 'MacOS' /
|
||||
'QtWebEngineProcess').resolve())
|
||||
no_custom_factor_rounding = not ('QT_SCALE_FACTOR_ROUNDING_POLICY' in os.environ
|
||||
and bool(os.environ['QT_SCALE_FACTOR_ROUNDING_POLICY'].strip()))
|
||||
if no_custom_factor_rounding:
|
||||
|
@ -502,9 +502,11 @@ def main():
|
|||
app = OpenLP()
|
||||
Registry.create()
|
||||
QtWidgets.QApplication.setOrganizationName('OpenLP')
|
||||
QtWidgets.QApplication.setOrganizationName('openlp.org')
|
||||
QtWidgets.QApplication.setApplicationName('OpenLP')
|
||||
QtWidgets.QApplication.setOrganizationDomain('openlp.org')
|
||||
if args.portable:
|
||||
# This has to be done here so that we can load the settings before instantiating the application object
|
||||
QtWidgets.QApplication.setApplicationName('OpenLPPortable')
|
||||
portable_path, settings = setup_portable_settings(args.portablepath)
|
||||
else:
|
||||
settings = Settings()
|
||||
|
@ -529,7 +531,6 @@ def main():
|
|||
font.setPointSizeF(font.pointSizeF() * application.devicePixelRatio())
|
||||
application.setFont(font)
|
||||
if args.portable:
|
||||
application.setApplicationName('OpenLPPortable')
|
||||
data_path = portable_path / 'Data'
|
||||
set_up_logging(portable_path / 'Other')
|
||||
set_up_web_engine_cache(portable_path / 'Other' / 'web_cache')
|
||||
|
@ -540,7 +541,6 @@ def main():
|
|||
settings.setValue('advanced/is portable', True)
|
||||
settings.sync()
|
||||
else:
|
||||
application.setApplicationName('OpenLP')
|
||||
set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
|
||||
set_up_web_engine_cache(AppLocation.get_directory(AppLocation.CacheDir) / 'web_cache')
|
||||
settings.init_default_shortcuts()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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': '',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -309,15 +309,15 @@ class Ui_EditSongDialog(object):
|
|||
self.verse_delete_button.setText(UiStrings().Delete)
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.lyrics_tab),
|
||||
translate('SongsPlugin.EditSongForm', 'Title && Lyrics'))
|
||||
self.authors_group_box.setTitle(SongStrings.Authors)
|
||||
self.authors_group_box.setTitle(SongStrings().Authors)
|
||||
self.author_add_button.setText(translate('SongsPlugin.EditSongForm', '&Add to Song'))
|
||||
self.author_edit_button.setText(translate('SongsPlugin.EditSongForm', '&Edit Author Type'))
|
||||
self.author_remove_button.setText(translate('SongsPlugin.EditSongForm', '&Remove'))
|
||||
self.maintenance_button.setText(translate('SongsPlugin.EditSongForm', '&Manage Authors, Topics, Songbooks'))
|
||||
self.topics_group_box.setTitle(SongStrings.Topics)
|
||||
self.topics_group_box.setTitle(SongStrings().Topics)
|
||||
self.topic_add_button.setText(translate('SongsPlugin.EditSongForm', 'A&dd to Song'))
|
||||
self.topic_remove_button.setText(translate('SongsPlugin.EditSongForm', 'R&emove'))
|
||||
self.songbook_group_box.setTitle(SongStrings.SongBooks)
|
||||
self.songbook_group_box.setTitle(SongStrings().SongBooks)
|
||||
self.songbook_add_button.setText(translate('SongsPlugin.EditSongForm', 'Add &to Song'))
|
||||
self.songbook_remove_button.setText(translate('SongsPlugin.EditSongForm', 'Re&move'))
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.authors_tab),
|
||||
|
@ -325,7 +325,7 @@ class Ui_EditSongDialog(object):
|
|||
self.theme_group_box.setTitle(UiStrings().Theme)
|
||||
self.theme_add_button.setText(translate('SongsPlugin.EditSongForm', 'New &Theme'))
|
||||
self.rights_group_box.setTitle(translate('SongsPlugin.EditSongForm', 'Copyright Information'))
|
||||
self.copyright_insert_button.setText(SongStrings.CopyrightSymbol)
|
||||
self.copyright_insert_button.setText(SongStrings().CopyrightSymbol)
|
||||
self.ccli_label.setText(UiStrings().CCLISongNumberLabel)
|
||||
self.comments_group_box.setTitle(translate('SongsPlugin.EditSongForm', 'Comments'))
|
||||
self.song_tab_widget.setTabText(self.song_tab_widget.indexOf(self.theme_tab),
|
||||
|
|
|
@ -278,6 +278,12 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
name=VerseType.translated_name(tag[0]),
|
||||
number=tag[1:]))
|
||||
return False
|
||||
if self.audio_list_widget.count() > 1:
|
||||
self.song_tab_widget.setCurrentIndex(3)
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file. Remove items from '
|
||||
'Linked Audio other than the one you wish to keep.'))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _validate_tags(self, tags, first_time=True):
|
||||
|
@ -911,7 +917,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
text = self.copyright_edit.text()
|
||||
pos = self.copyright_edit.cursorPosition()
|
||||
sign = SongStrings.CopyrightSymbol
|
||||
sign = SongStrings().CopyrightSymbol
|
||||
text = text[:pos] + sign + text[pos:]
|
||||
self.copyright_edit.setText(text)
|
||||
self.copyright_edit.setFocus()
|
||||
|
@ -942,6 +948,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
Loads file(s) from the filesystem.
|
||||
"""
|
||||
if self.audio_list_widget.count() > 0:
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file.'))
|
||||
return
|
||||
filters = '{text} (*)'.format(text=UiStrings().AllFiles)
|
||||
file_paths, filter_used = FileDialog.getOpenFileNames(
|
||||
parent=self, caption=translate('SongsPlugin.EditSongForm', 'Open File(s)'), filter=filters)
|
||||
|
@ -954,6 +964,10 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
|
|||
"""
|
||||
Loads file(s) from the media plugin.
|
||||
"""
|
||||
if self.audio_list_widget.count() > 0:
|
||||
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
|
||||
'Cannot link more than one audio file.'))
|
||||
return
|
||||
if self.media_form.exec():
|
||||
for file_path in self.media_form.get_selected_files():
|
||||
item = QtWidgets.QListWidgetItem(file_path.name)
|
||||
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -141,10 +141,10 @@ class Ui_SongMaintenanceDialog(object):
|
|||
"""
|
||||
Translate the UI on the fly.
|
||||
"""
|
||||
song_maintenance_dialog.setWindowTitle(SongStrings.SongMaintenance)
|
||||
self.authors_list_item.setText(SongStrings.Authors)
|
||||
self.topics_list_item.setText(SongStrings.Topics)
|
||||
self.books_list_item.setText(SongStrings.SongBooks)
|
||||
song_maintenance_dialog.setWindowTitle(SongStrings().SongMaintenance)
|
||||
self.authors_list_item.setText(SongStrings().Authors)
|
||||
self.topics_list_item.setText(SongStrings().Topics)
|
||||
self.books_list_item.setText(SongStrings().SongBooks)
|
||||
self.add_author_button.setText(UiStrings().Add)
|
||||
self.edit_author_button.setText(UiStrings().Edit)
|
||||
self.delete_author_button.setText(UiStrings().Delete)
|
||||
|
@ -154,7 +154,7 @@ class Ui_SongMaintenanceDialog(object):
|
|||
self.add_book_button.setText(UiStrings().Add)
|
||||
self.edit_book_button.setText(UiStrings().Edit)
|
||||
self.delete_book_button.setText(UiStrings().Delete)
|
||||
type_list_width = max(self.fontMetrics().width(SongStrings.Authors),
|
||||
self.fontMetrics().width(SongStrings.Topics),
|
||||
self.fontMetrics().width(SongStrings.SongBooks))
|
||||
type_list_width = max(self.fontMetrics().width(SongStrings().Authors),
|
||||
self.fontMetrics().width(SongStrings().Topics),
|
||||
self.fontMetrics().width(SongStrings().SongBooks))
|
||||
self.type_list_widget.setFixedWidth(type_list_width + self.type_list_widget.iconSize().width() + 32)
|
||||
|
|
|
@ -125,7 +125,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
|||
self.song_progress_bar.setValue(2)
|
||||
song_filename = self.current_download_item.downloadDirectory() + '/' \
|
||||
+ self.current_download_item.downloadFileName()
|
||||
song_file = open(song_filename, 'rt')
|
||||
song_file = open(song_filename, 'rt', encoding='utf-8')
|
||||
song_content = song_file.read()
|
||||
song_file.seek(0)
|
||||
if self.check_for_duplicate(song_content):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -166,28 +166,29 @@ class SongFormat(object):
|
|||
EasyWorshipDB = 7
|
||||
EasyWorshipSqliteDB = 8
|
||||
EasyWorshipService = 9
|
||||
FoilPresenter = 10
|
||||
LiveWorship = 11
|
||||
Lyrix = 12
|
||||
MediaShout = 13
|
||||
OpenSong = 14
|
||||
OPSPro = 15
|
||||
PowerPraise = 16
|
||||
PowerSong = 17
|
||||
PresentationManager = 18
|
||||
ProPresenter = 19
|
||||
SingingTheFaith = 20
|
||||
SongBeamer = 21
|
||||
SongPro = 22
|
||||
SongShowPlus = 23
|
||||
SongsOfFellowship = 24
|
||||
SundayPlus = 25
|
||||
VideoPsalm = 26
|
||||
WordsOfWorship = 27
|
||||
WorshipAssistant = 28
|
||||
WorshipCenterPro = 29
|
||||
ZionWorx = 30
|
||||
Datasoul = 31
|
||||
EasyWorshipServiceSqliteDB = 10
|
||||
FoilPresenter = 11
|
||||
LiveWorship = 12
|
||||
Lyrix = 13
|
||||
MediaShout = 14
|
||||
OpenSong = 15
|
||||
OPSPro = 16
|
||||
PowerPraise = 17
|
||||
PowerSong = 18
|
||||
PresentationManager = 19
|
||||
ProPresenter = 20
|
||||
SingingTheFaith = 21
|
||||
SongBeamer = 22
|
||||
SongPro = 23
|
||||
SongShowPlus = 24
|
||||
SongsOfFellowship = 25
|
||||
SundayPlus = 26
|
||||
VideoPsalm = 27
|
||||
WordsOfWorship = 28
|
||||
WorshipAssistant = 29
|
||||
WorshipCenterPro = 30
|
||||
ZionWorx = 31
|
||||
Datasoul = 32
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
|
@ -278,6 +279,14 @@ class SongFormat(object):
|
|||
'filter': '{text} (*.ews)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship 2007/2009 Service File'))
|
||||
},
|
||||
EasyWorshipServiceSqliteDB: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship 6/7 Service File',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '{text} (*.ewsx)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'EasyWorship 6/7 Service File'))
|
||||
},
|
||||
FoilPresenter: {
|
||||
'class': FoilPresenterImport,
|
||||
'name': 'Foilpresenter',
|
||||
|
@ -487,6 +496,7 @@ class SongFormat(object):
|
|||
SongFormat.EasyWorshipDB,
|
||||
SongFormat.EasyWorshipSqliteDB,
|
||||
SongFormat.EasyWorshipService,
|
||||
SongFormat.EasyWorshipServiceSqliteDB,
|
||||
SongFormat.FoilPresenter,
|
||||
SongFormat.LiveWorship,
|
||||
SongFormat.Lyrix,
|
||||
|
|
|
@ -56,7 +56,7 @@ class DatasoulImport(SongImport):
|
|||
tree = objectify.parse(str(file_path), parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
song_xml = tree.getroot()
|
||||
if song_xml.tag != 'Song':
|
||||
|
|
|
@ -93,16 +93,16 @@ class DreamBeamImport(SongImport):
|
|||
parsed_file = etree.parse(xml_file, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
file_str = etree.tostring(parsed_file)
|
||||
if not file_str:
|
||||
log.exception('Could not find XML in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
xml = file_str.decode()
|
||||
song_xml = objectify.fromstring(xml)
|
||||
|
@ -151,7 +151,7 @@ class DreamBeamImport(SongImport):
|
|||
author_copyright = song_xml.Text2.Text.text
|
||||
if author_copyright:
|
||||
author_copyright = str(author_copyright)
|
||||
if author_copyright.find(SongStrings.CopyrightSymbol) >= 0:
|
||||
if author_copyright.find(SongStrings().CopyrightSymbol) >= 0:
|
||||
self.add_copyright(author_copyright)
|
||||
else:
|
||||
self.parse_author(author_copyright)
|
||||
|
|
|
@ -54,16 +54,16 @@ class EasySlidesImport(SongImport):
|
|||
parsed_file = etree.parse(xml_file, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
file_str = etree.tostring(parsed_file)
|
||||
if not file_str:
|
||||
log.exception('Could not find XML in file {name}'.format(name=self.import_source))
|
||||
self.log_error(self.import_source, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
|
||||
return
|
||||
xml = file_str.decode()
|
||||
song_xml = objectify.fromstring(xml)
|
||||
|
|
|
@ -28,6 +28,8 @@ import sqlite3
|
|||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding, strip_rtf
|
||||
|
@ -83,6 +85,8 @@ class EasyWorshipSongImport(SongImport):
|
|||
self.import_ews()
|
||||
elif ext == '.db':
|
||||
self.import_db()
|
||||
elif ext == '.ewsx':
|
||||
self.import_ewsx()
|
||||
else:
|
||||
self.import_sqlite_db()
|
||||
except Exception:
|
||||
|
@ -346,6 +350,65 @@ class EasyWorshipSongImport(SongImport):
|
|||
db_file.close()
|
||||
self.memo_file.close()
|
||||
|
||||
def import_ewsx(self):
|
||||
"""
|
||||
Imports songs from an EasyWorship 6/7 service file, which is just a zip file with an Sqlite DB with text
|
||||
resources. Non-text recources is also in the zip file, but is ignored.
|
||||
"""
|
||||
invalid_ewsx_msg = translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'This is not a valid Easy Worship 6/7 service file.')
|
||||
# Open ewsx file if it exists
|
||||
if not self.import_source.is_file():
|
||||
log.debug('Given ewsx file does not exists.')
|
||||
return
|
||||
tmp_db_file = NamedTemporaryFile(delete=False)
|
||||
with ZipFile(self.import_source, 'r') as eswx_file:
|
||||
db_zfile = eswx_file.open('main.db')
|
||||
# eswx has bad CRC for the database for some reason (custom CRC?), so skip the CRC
|
||||
db_zfile._expected_crc = None
|
||||
db_data = db_zfile.read()
|
||||
tmp_db_file.write(db_data)
|
||||
tmp_db_file.close()
|
||||
ewsx_conn = sqlite3.connect(tmp_db_file.file.name)
|
||||
if ewsx_conn is None:
|
||||
self.log_error(self.import_source, invalid_ewsx_msg)
|
||||
return
|
||||
ewsx_db = ewsx_conn.cursor()
|
||||
# Take a stab at how text is encoded
|
||||
self.encoding = 'cp1252'
|
||||
self.encoding = retrieve_windows_encoding(self.encoding)
|
||||
if not self.encoding:
|
||||
log.debug('No encoding set.')
|
||||
return
|
||||
# get list of songs in service file, presentation_type=6 means songs
|
||||
songs_exec = ewsx_db.execute('SELECT rowid, title, author, copyright, reference_number '
|
||||
'FROM presentation WHERE presentation_type=6;')
|
||||
songs = songs_exec.fetchall()
|
||||
for song in songs:
|
||||
self.title = title = song[1]
|
||||
self.author = song[2]
|
||||
self.copyright = song[3]
|
||||
self.ccli_number = song[4]
|
||||
# get slides for the song, element_type=6 means songs, element_style_type=4 means song text
|
||||
slides = ewsx_db.execute('SELECT rt.rtf '
|
||||
'FROM element as e '
|
||||
'JOIN slide as s ON e.slide_id = s.rowid '
|
||||
'JOIN resource_text as rt ON rt.resource_id = e.foreground_resource_id '
|
||||
'WHERE e.element_type=6 AND e.element_style_type=4 AND s.presentation_id = ? '
|
||||
'ORDER BY s.order_index;', (song[0],))
|
||||
for slide in slides:
|
||||
if slide:
|
||||
self.set_song_import_object(self.author, slide[0].encode())
|
||||
# save song
|
||||
if not self.finish():
|
||||
self.log_error(self.import_source,
|
||||
translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'"{title}" could not be imported. {entry}').
|
||||
format(title=title, entry=self.entry_error_log))
|
||||
# close database handles
|
||||
ewsx_conn.close()
|
||||
Path(tmp_db_file.file.name).unlink()
|
||||
|
||||
def import_sqlite_db(self):
|
||||
"""
|
||||
Import the songs from an EasyWorship 6 SQLite database
|
||||
|
|
|
@ -125,7 +125,7 @@ class FoilPresenterImport(SongImport):
|
|||
xml = etree.tostring(parsed_file).decode()
|
||||
self.foil_presenter.xml_to_song(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
log.exception('XML syntax error in file {path}'.format(path=file_path))
|
||||
except AttributeError:
|
||||
self.log_error(file_path, translate('SongsPlugin.FoilPresenterSongImport',
|
||||
|
|
|
@ -72,7 +72,7 @@ class LiveWorshipImport(SongImport):
|
|||
try:
|
||||
self.root = etree.fromstring(xml_content, parser)
|
||||
except etree.XMLSyntaxError:
|
||||
self.log_error(self.dump_file, SongStrings.XMLSyntaxError)
|
||||
self.log_error(self.dump_file, SongStrings().XMLSyntaxError)
|
||||
log.exception('XML syntax error in file {path}'.format(path=str(self.dump_file)))
|
||||
|
||||
def extract_songs(self):
|
||||
|
|
|
@ -73,7 +73,7 @@ class OpenLyricsImport(SongImport):
|
|||
self.open_lyrics.xml_to_song(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {path}'.format(path=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
except OpenLyricsError as exception:
|
||||
log.exception('OpenLyricsException {error:d} in file {name}: {text}'.format(error=exception.type,
|
||||
name=file_path,
|
||||
|
|
|
@ -129,7 +129,7 @@ class OpenSongImport(SongImport):
|
|||
try:
|
||||
tree = objectify.parse(file)
|
||||
except (etree.Error, etree.LxmlError):
|
||||
self.log_error(file.name, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file.name, SongStrings().XMLSyntaxError)
|
||||
log.exception('Error parsing XML')
|
||||
return
|
||||
root = tree.getroot()
|
||||
|
|
|
@ -50,11 +50,11 @@ class PowerPraiseImport(SongImport):
|
|||
root = objectify.parse(xml_file).getroot()
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
self.process_song(root, file_path)
|
||||
|
||||
|
|
|
@ -54,11 +54,11 @@ class ProPresenterImport(SongImport):
|
|||
root = objectify.parse(xml_file).getroot()
|
||||
except etree.XMLSyntaxError:
|
||||
log.exception('XML syntax error in file {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
except UnicodeDecodeError:
|
||||
log.exception('Unreadable characters in {name}'.format(name=file_path))
|
||||
self.log_error(file_path, SongStrings.XMLSyntaxError)
|
||||
self.log_error(file_path, SongStrings().XMLSyntaxError)
|
||||
continue
|
||||
try:
|
||||
self.process_song(root, file_path)
|
||||
|
|
|
@ -104,7 +104,7 @@ class SongImport(QtCore.QObject):
|
|||
self.verse_counts = {}
|
||||
self.copyright_string = translate('SongsPlugin.SongImport', 'copyright')
|
||||
|
||||
def log_error(self, file_path, reason=SongStrings.SongIncomplete):
|
||||
def log_error(self, file_path, reason=SongStrings().SongIncomplete):
|
||||
"""
|
||||
This should be called, when a song could not be imported.
|
||||
|
||||
|
@ -151,11 +151,11 @@ class SongImport(QtCore.QObject):
|
|||
:param text: Some text
|
||||
"""
|
||||
lines = text.split('\n')
|
||||
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings.CopyrightSymbol)) >= 0:
|
||||
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings().CopyrightSymbol)) >= 0:
|
||||
copyright_found = False
|
||||
for line in lines:
|
||||
if (copyright_found or line.lower().find(self.copyright_string) >= 0 or
|
||||
line.find(str(SongStrings.CopyrightSymbol)) >= 0):
|
||||
line.find(str(SongStrings().CopyrightSymbol)) >= 0):
|
||||
copyright_found = True
|
||||
self.add_copyright(line)
|
||||
else:
|
||||
|
|
|
@ -124,7 +124,7 @@ class SongMediaItem(MediaManagerItem):
|
|||
def retranslate_ui(self):
|
||||
self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
|
||||
self.search_text_button.setText(UiStrings().Search)
|
||||
self.maintenance_action.setText(SongStrings.SongMaintenance)
|
||||
self.maintenance_action.setText(SongStrings().SongMaintenance)
|
||||
self.maintenance_action.setToolTip(translate('SongsPlugin.MediaItem',
|
||||
'Maintain the lists of authors, topics and books.'))
|
||||
|
||||
|
@ -145,11 +145,11 @@ class SongMediaItem(MediaManagerItem):
|
|||
(SongSearch.Lyrics, UiIcons().search_lyrics,
|
||||
translate('SongsPlugin.MediaItem', 'Lyrics'),
|
||||
translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
|
||||
(SongSearch.Authors, UiIcons().user, SongStrings.Authors,
|
||||
(SongSearch.Authors, UiIcons().user, SongStrings().Authors,
|
||||
translate('SongsPlugin.MediaItem', 'Search Authors...')),
|
||||
(SongSearch.Topics, UiIcons().light_bulb, SongStrings.Topics,
|
||||
(SongSearch.Topics, UiIcons().light_bulb, SongStrings().Topics,
|
||||
translate('SongsPlugin.MediaItem', 'Search Topics...')),
|
||||
(SongSearch.Books, UiIcons().address, SongStrings.SongBooks,
|
||||
(SongSearch.Books, UiIcons().address, SongStrings().SongBooks,
|
||||
translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
|
||||
(SongSearch.Themes, UiIcons().theme, UiStrings().Themes, UiStrings().SearchThemes),
|
||||
(SongSearch.Copyright, UiIcons().copyright,
|
||||
|
@ -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))
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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())
|
|
@ -95,6 +95,7 @@ MODULES = [
|
|||
'pymediainfo',
|
||||
'vlc',
|
||||
'qrcode',
|
||||
'packaging',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -105,6 +105,7 @@ using a computer and a display/projector.""",
|
|||
'flask-cors',
|
||||
'lxml',
|
||||
'Mako',
|
||||
'packaging',
|
||||
'platformdirs',
|
||||
'PyICU',
|
||||
'pymediainfo >= 2.2',
|
||||
|
|
|
@ -29,7 +29,7 @@ from openlp.core.state import State
|
|||
from openlp.core.lib.plugin import PluginStatus, StringContent
|
||||
|
||||
|
||||
def test_plugins_returns_list(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_plugins_returns_list(flask_client: FlaskClient):
|
||||
State().load_settings()
|
||||
res = flask_client.get('/api/v2/core/plugins').get_json()
|
||||
assert len(res) == 0
|
||||
|
@ -52,14 +52,30 @@ def test_plugins_returns_list(flask_client: FlaskClient, registry: Registry, set
|
|||
assert res[0]['name'] == plugin.text_strings[StringContent.Name]['plural']
|
||||
|
||||
|
||||
def test_system_information(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_system_information(flask_client: FlaskClient, settings: Settings):
|
||||
Registry().get('settings_thread').setValue('api/authentication enabled', False)
|
||||
res = flask_client.get('/api/v2/core/system').get_json()
|
||||
assert res['websocket_port'] > 0
|
||||
assert not res['login_required']
|
||||
|
||||
|
||||
def test_poll_backend(registry: Registry, settings: Settings):
|
||||
def test_shortcuts(flask_client: FlaskClient, settings: Settings):
|
||||
action = 'shortcuts/aboutItem'
|
||||
shortcut = 'Ctrl+F1'
|
||||
Registry().get('settings_thread').setValue(action, shortcut)
|
||||
res = flask_client.get('/api/v2/core/shortcuts')
|
||||
assert res.status_code == 200
|
||||
assert res.get_json()[0]['action'] == action.removeprefix('shortcuts/')
|
||||
assert res.get_json()[0]['shortcut'] == shortcut
|
||||
|
||||
|
||||
def test_language(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.get('/api/v2/core/language')
|
||||
assert res.status_code == 200
|
||||
assert res.get_json()['language']
|
||||
|
||||
|
||||
def test_poll_backend(settings: Settings):
|
||||
"""
|
||||
Test the raw poll function returns the correct JSON
|
||||
"""
|
||||
|
@ -100,12 +116,12 @@ def test_login_without_data_returns_400(flask_client: FlaskClient):
|
|||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_login_with_invalid_credetials_returns_401(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_login_with_invalid_credetials_returns_401(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='invalid'))
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_login_with_valid_credetials_returns_token(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_login_with_valid_credetials_returns_token(flask_client: FlaskClient, settings: Settings):
|
||||
Registry().register('authentication_token', 'foobar')
|
||||
res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='password'))
|
||||
assert res.status_code == 200
|
||||
|
@ -125,7 +141,7 @@ def test_retrieving_image(flask_client: FlaskClient):
|
|||
assert res['binary_image'] != ''
|
||||
|
||||
|
||||
def test_toggle_display_requires_login(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_requires_login(flask_client: FlaskClient, settings: Settings):
|
||||
settings.setValue('api/authentication enabled', True)
|
||||
Registry().register('authentication_token', 'foobar')
|
||||
res = flask_client.post('/api/v2/core/display')
|
||||
|
@ -138,18 +154,17 @@ def test_toggle_display_does_not_allow_get(flask_client: FlaskClient):
|
|||
assert res.status_code == 405
|
||||
|
||||
|
||||
def test_toggle_display_invalid_action(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_invalid_action(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/display', json={'display': 'foo'})
|
||||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_toggle_display_no_data(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_toggle_display_no_data(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.post('/api/v2/core/display', json={})
|
||||
assert res.status_code == 400
|
||||
|
||||
|
||||
def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClient, registry: Registry,
|
||||
settings: Settings):
|
||||
def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClient, settings: Settings):
|
||||
class FakeController:
|
||||
class Emitter:
|
||||
def emit(self, value):
|
||||
|
@ -162,7 +177,7 @@ def test_toggle_display_valid_action_updates_controller(flask_client: FlaskClien
|
|||
assert controller.slidecontroller_toggle_display.set == 'show'
|
||||
|
||||
|
||||
def test_cors_headers_are_present(flask_client: FlaskClient, registry: Registry, settings: Settings):
|
||||
def test_cors_headers_are_present(flask_client: FlaskClient, settings: Settings):
|
||||
res = flask_client.get('/api/v2/core/system')
|
||||
assert 'Access-Control-Allow-Origin' in res.headers
|
||||
assert res.headers['Access-Control-Allow-Origin'] == '*'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -21,10 +21,16 @@
|
|||
"""
|
||||
This module contains tests for the lib submodule of the Songs plugin.
|
||||
"""
|
||||
import logging
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib.db import Author
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -33,6 +39,17 @@ def edit_song_form():
|
|||
return EditSongForm(None, MagicMock(), MagicMock())
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def edit_song_form_with_ui(settings: Settings) -> EditSongForm:
|
||||
main_window = QtWidgets.QMainWindow()
|
||||
Registry().register('main_window', main_window)
|
||||
Registry().register('theme_manager', MagicMock())
|
||||
form = EditSongForm(None, main_window, MagicMock())
|
||||
yield form
|
||||
del form
|
||||
del main_window
|
||||
|
||||
|
||||
def test_validate_matching_tags(edit_song_form):
|
||||
# Given a set of tags
|
||||
tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']
|
||||
|
@ -86,3 +103,104 @@ def test_load_objects(mocked_set_case_insensitive_completer, edit_song_form, set
|
|||
mocked_set_case_insensitive_completer.assert_called_once_with(mocked_cache, mocked_combo)
|
||||
mocked_combo.setCurrentIndex.assert_called_once_with(-1)
|
||||
mocked_combo.setCurrentText.assert_called_once_with('')
|
||||
|
||||
|
||||
def test_add_multiple_audio_from_file(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that not more than one Linked Audio item can be added
|
||||
"""
|
||||
# GIVEN: A Linked Audio list with 1 item and mocked error message handler
|
||||
item = QtWidgets.QListWidgetItem('Audio file')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Add File is clicked
|
||||
edit_song_form_with_ui.on_audio_add_from_file_button_clicked()
|
||||
|
||||
# THEN: A call to show an error message should have been made and no items should have been added
|
||||
mocked_error_message.assert_called_once()
|
||||
assert edit_song_form_with_ui.audio_list_widget.count() == 1
|
||||
|
||||
|
||||
def test_add_multiple_audio_from_media(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that not more than one Linked Audio item can be added
|
||||
"""
|
||||
# GIVEN: A Linked Audio list with 1 item and mocked error message handler
|
||||
item = QtWidgets.QListWidgetItem('Audio file')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Add Media is clicked
|
||||
edit_song_form_with_ui.on_audio_add_from_media_button_clicked()
|
||||
|
||||
# THEN: A call to show an error message should have been made and no items should have been added
|
||||
mocked_error_message.assert_called_once()
|
||||
assert edit_song_form_with_ui.audio_list_widget.count() == 1
|
||||
|
||||
|
||||
def test_validate_song_multiple_audio(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that a form with multiple Linked Audio items does not pass validation
|
||||
"""
|
||||
# GIVEN: A form with title, lyrics, an author, and 2 Linked Audio items
|
||||
edit_song_form_with_ui.title_edit.setText('Song Title')
|
||||
verse_def = '{tag}{number}'.format(tag=VerseType.tags[0], number=1)
|
||||
song_lyrics = 'Song Lyrics'
|
||||
verse_item = QtWidgets.QTableWidgetItem(song_lyrics)
|
||||
verse_item.setData(QtCore.Qt.UserRole, verse_def)
|
||||
verse_item.setText(song_lyrics)
|
||||
edit_song_form_with_ui.verse_list_widget.setRowCount(1)
|
||||
edit_song_form_with_ui.verse_list_widget.setItem(0, 0, verse_item)
|
||||
item_1 = QtWidgets.QListWidgetItem('Audio file 1')
|
||||
item_2 = QtWidgets.QListWidgetItem('Audio file 2')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_1)
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_2)
|
||||
author = Author(first_name='', last_name='', display_name='Author')
|
||||
author_type = edit_song_form_with_ui.author_types_combo_box.itemData(0)
|
||||
edit_song_form_with_ui._add_author_to_list(author, author_type)
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Song is validated
|
||||
song_valid = edit_song_form_with_ui._validate_song()
|
||||
|
||||
# THEN: It should not be valid
|
||||
assert song_valid is False
|
||||
|
||||
|
||||
def test_validate_song_one_audio(edit_song_form_with_ui: EditSongForm):
|
||||
"""
|
||||
Test that a form with one Linked Audio item passes validation
|
||||
"""
|
||||
# GIVEN: A form with title, lyrics, an author, and 1 Linked Audio item
|
||||
edit_song_form_with_ui.title_edit.setText('Song Title')
|
||||
verse_def = '{tag}{number}'.format(tag=VerseType.tags[0], number=1)
|
||||
song_lyrics = 'Song Lyrics'
|
||||
verse_item = QtWidgets.QTableWidgetItem(song_lyrics)
|
||||
verse_item.setData(QtCore.Qt.UserRole, verse_def)
|
||||
verse_item.setText(song_lyrics)
|
||||
edit_song_form_with_ui.verse_list_widget.setRowCount(1)
|
||||
edit_song_form_with_ui.verse_list_widget.setItem(0, 0, verse_item)
|
||||
item_1 = QtWidgets.QListWidgetItem('Audio file 1')
|
||||
edit_song_form_with_ui.audio_list_widget.addItem(item_1)
|
||||
author = Author(first_name='', last_name='', display_name='Author')
|
||||
author_type = edit_song_form_with_ui.author_types_combo_box.itemData(0)
|
||||
edit_song_form_with_ui._add_author_to_list(author, author_type)
|
||||
# If the validation does fail for some reason it will likely try to display an error message,
|
||||
# so make sure error_message exists to avoid an error
|
||||
mocked_error_message = MagicMock()
|
||||
Registry().get('main_window').error_message = mocked_error_message
|
||||
|
||||
# WHEN: Song is validated
|
||||
song_valid = edit_song_form_with_ui._validate_song()
|
||||
|
||||
# Log the error message to help determine the cause in the case validation failed
|
||||
if mocked_error_message.called:
|
||||
_title, message = mocked_error_message.call_args.args
|
||||
logging.error('Validation error message: {message}'.format(message=message))
|
||||
|
||||
# THEN: It should be valid
|
||||
assert song_valid is True
|
||||
|
|
|
@ -498,6 +498,48 @@ def test_ews_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongIm
|
|||
mocked_finish.assert_called_with()
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.SongImport')
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding')
|
||||
def test_ewsx_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongImport: MagicMock,
|
||||
registry: Registry, settings: Settings):
|
||||
"""
|
||||
Test the actual import of song from ewsx file and check that the imported data is correct.
|
||||
"""
|
||||
|
||||
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
|
||||
# and mocked out "author", "add_copyright", "add_verse", "finish" methods.
|
||||
mocked_retrieve_windows_encoding.return_value = 'cp1252'
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
mocked_add_author = MagicMock()
|
||||
mocked_add_verse = MagicMock()
|
||||
mocked_finish = MagicMock()
|
||||
mocked_title = MagicMock()
|
||||
mocked_finish.return_value = True
|
||||
importer = EasyWorshipSongImportLogger(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = False
|
||||
importer.add_author = mocked_add_author
|
||||
importer.add_verse = mocked_add_verse
|
||||
importer.title = mocked_title
|
||||
importer.finish = mocked_finish
|
||||
importer.topics = []
|
||||
|
||||
# WHEN: Importing ews file
|
||||
importer.import_source = str(TEST_PATH / 'test1.ewsx')
|
||||
import_result = importer.do_import()
|
||||
|
||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||
# called.
|
||||
title = EWS_SONG_TEST_DATA['title']
|
||||
assert import_result is None, 'do_import should return None when it has completed'
|
||||
assert title in importer._title_assignment_list, 'title for should be "%s"' % title
|
||||
mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0])
|
||||
for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']:
|
||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||
mocked_finish.assert_called_with()
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.importers.easyworship.SongImport')
|
||||
def test_import_rtf_unescaped_unicode(MockSongImport: MagicMock, registry: Registry, settings: Settings):
|
||||
"""
|
||||
|
|
|
@ -111,14 +111,14 @@ def test_can_parse_file_having_a_processing_instruction(mocked_logger: MagicMock
|
|||
# otherwise we don't care about it now (but should in other tests...)
|
||||
assert ex is not etree.XMLSyntaxError
|
||||
|
||||
# THEN: the importer's log_error method was never called with SongStrings.XMLSyntaxError as its second
|
||||
# THEN: the importer's log_error method was never called with SongStrings().XMLSyntaxError as its second
|
||||
# positional argument
|
||||
if importer.log_error.called:
|
||||
for call_args in importer.log_error.call_args_list:
|
||||
args = call_args[0]
|
||||
# there are at least two positional arguments
|
||||
if len(args) > 1:
|
||||
assert args[1] is not SongStrings.XMLSyntaxError
|
||||
assert args[1] is not SongStrings().XMLSyntaxError
|
||||
|
||||
# THEN: the logger's 'exception' method was never called with a first positional argument
|
||||
# which is a string and starts with 'XML syntax error in file'
|
||||
|
|
|
@ -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.
Loading…
Reference in New Issue