diff --git a/.bzrignore b/.bzrignore
index b91d4fc93..a0c3f0b4f 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -6,6 +6,8 @@
*.ropeproject
*.e4*
.eric4project
+.komodotools
+*.komodoproject
list
openlp.org 2.0.e4*
documentation/build/html
@@ -30,3 +32,4 @@ tests.kdev4
*.orig
__pycache__
*.dll
+.directory
diff --git a/MANIFEST.in b/MANIFEST.in
index 35e83e30f..be81efb23 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -5,6 +5,8 @@ recursive-include openlp *.html
recursive-include openlp *.js
recursive-include openlp *.css
recursive-include openlp *.png
+recursive-include openlp *.ps
+recursive-include openlp *.json
recursive-include documentation *
recursive-include resources *
recursive-include scripts *
diff --git a/README.txt b/README.txt
index b937e1d5f..04294b1b8 100644
--- a/README.txt
+++ b/README.txt
@@ -1,16 +1,15 @@
-OpenLP 2.0
-==========
+OpenLP
+======
You're probably reading this because you've just downloaded the source code for
-OpenLP 2.0. If you are looking for the installer file, please go to the download
+OpenLP. If you are looking for the installer file, please go to the download
page on the web site::
- http://openlp.org/en/download.html
+ http://openlp.org/download
If you're looking for how to contribute to OpenLP, then please look at the
OpenLP wiki::
http://wiki.openlp.org/
-Thanks for downloading OpenLP 2.0!
-
+Thanks for downloading OpenLP!
diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py
index 1fce25000..2bc4027b6 100644
--- a/openlp/core/common/applocation.py
+++ b/openlp/core/common/applocation.py
@@ -110,7 +110,7 @@ class AppLocation(object):
:param extension:
Defaults to *None*. The extension to search for. For example::
- u'.png'
+ '.png'
"""
path = AppLocation.get_data_path()
if section:
diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py
index 590452f73..3b7b31ca1 100644
--- a/openlp/core/common/settings.py
+++ b/openlp/core/common/settings.py
@@ -68,8 +68,7 @@ class Settings(QtCore.QSettings):
``__obsolete_settings__``
Each entry is structured in the following way::
- (u'general/enable slide loop', u'advanced/slide limits',
- [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
+ ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
The first entry is the *old key*; it will be removed.
diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py
index 9561baff4..a7cba33a0 100644
--- a/openlp/core/lib/__init__.py
+++ b/openlp/core/lib/__init__.py
@@ -296,8 +296,7 @@ def create_separated_list(string_list):
:param string_list: List of unicode strings
"""
- if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and \
- LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
+ if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
return QtCore.QLocale().createSeparatedList(string_list)
if not string_list:
return ''
diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py
index 1014c994d..d67c05c42 100644
--- a/openlp/core/lib/db.py
+++ b/openlp/core/lib/db.py
@@ -194,6 +194,7 @@ class Manager(object):
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
except (SQLAlchemyError, DBAPIError):
log.exception('Error loading database: %s', self.db_url)
+ return
if db_ver > up_ver:
critical_error_message_box(
translate('OpenLP.Manager', 'Database Error'),
@@ -215,7 +216,7 @@ class Manager(object):
Save an object to the database
:param object_instance: The object to save
- :param commit: Commit the session with this object
+ :param commit: Commit the session with this object
"""
for try_count in range(3):
try:
diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py
index 1f459524c..8cd13088d 100644
--- a/openlp/core/lib/plugin.py
+++ b/openlp/core/lib/plugin.py
@@ -129,7 +129,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
class MyPlugin(Plugin):
def __init__(self):
- super(MyPlugin, self).__init__('MyPlugin', version=u'0.1')
+ super(MyPlugin, self).__init__('MyPlugin', version='0.1')
:param name: Defaults to *None*. The name of the plugin.
:param default_settings: A dict containing the plugin's settings. The value to each key is the default value
diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py
index 233af3784..71a1f6058 100644
--- a/openlp/core/lib/renderer.py
+++ b/openlp/core/lib/renderer.py
@@ -248,6 +248,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
elif item.is_capable(ItemCapabilities.CanSoftBreak):
pages = []
if '[---]' in text:
+ # Remove two or more option slide breaks next to each other (causing infinite loop).
+ while '\n[---]\n[---]\n' in text:
+ text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
while True:
slides = text.split('\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
@@ -392,7 +395,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
off when displayed.
:param lines: The text to be fitted on the slide split into lines.
- :param line_end: The text added after each line. Either ``u' '`` or ``u'
``.
+ :param line_end: The text added after each line. Either ``' '`` or ``'
``.
"""
formatted = []
previous_html = ''
@@ -416,7 +419,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
processed word by word. This is sometimes need for **bible** verses.
:param lines: The text to be fitted on the slide split into lines.
- :param line_end: The text added after each line. Either ``u' '`` or ``u'
``. This is needed for **bibles**.
+ :param line_end: The text added after each line. Either ``' '`` or ``'
``. This is needed for **bibles**.
"""
formatted = []
previous_html = ''
@@ -453,7 +456,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
- (u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'')
+ ('{st}{r}Text text text{/r}{/st}', '{st}{r}', '')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
@@ -500,8 +503,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
The text contains html.
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The elements can contain formatting tags.
- :param separator: The separator for the elements. For lines this is ``u'
'`` and for words this is ``u' '``.
- :param line_end: The text added after each "element line". Either ``u' '`` or ``u'
``. This is needed for
+ :param separator: The separator for the elements. For lines this is ``'
'`` and for words this is ``' '``.
+ :param line_end: The text added after each "element line". Either ``' '`` or ``'
``. This is needed for
bibles.
"""
smallest_index = 0
diff --git a/openlp/core/lib/screen.py b/openlp/core/lib/screen.py
index 17ead5346..d05200f2f 100644
--- a/openlp/core/lib/screen.py
+++ b/openlp/core/lib/screen.py
@@ -63,8 +63,7 @@ class ScreenList(object):
"""
Initialise the screen list.
- ``desktop``
- A ``QDesktopWidget`` object.
+ :param desktop: A QDesktopWidget object.
"""
screen_list = cls()
screen_list.desktop = desktop
@@ -136,7 +135,7 @@ class ScreenList(object):
Returns a list with the screens. This should only be used to display
available screens to the user::
- [u'Screen 1 (primary)', u'Screen 2']
+ ['Screen 1 (primary)', 'Screen 2']
"""
screen_list = []
for screen in self.screen_list:
@@ -153,9 +152,9 @@ class ScreenList(object):
:param screen: A dict with the screen properties::
{
- u'primary': True,
- u'number': 0,
- u'size': PyQt4.QtCore.QRect(0, 0, 1024, 768)
+ 'primary': True,
+ 'number': 0,
+ 'size': PyQt4.QtCore.QRect(0, 0, 1024, 768)
}
"""
log.info('Screen %d found with resolution %s' % (screen['number'], screen['size']))
diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py
index 276a073bb..251e0657c 100644
--- a/openlp/core/ui/aboutdialog.py
+++ b/openlp/core/ui/aboutdialog.py
@@ -44,7 +44,7 @@ class Ui_AboutDialog(object):
Set up the UI for the dialog.
"""
about_dialog.setObjectName('about_dialog')
- about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
+ about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg'))
self.about_dialog_layout = QtGui.QVBoxLayout(about_dialog)
self.about_dialog_layout.setObjectName('about_dialog_layout')
self.logo_label = QtGui.QLabel(about_dialog)
diff --git a/openlp/core/ui/aboutform.py b/openlp/core/ui/aboutform.py
index e971bbec4..3825312bd 100644
--- a/openlp/core/ui/aboutform.py
+++ b/openlp/core/ui/aboutform.py
@@ -30,7 +30,7 @@
The About dialog.
"""
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtGui
from .aboutdialog import Ui_AboutDialog
from openlp.core.lib import translate
diff --git a/openlp/core/ui/exceptiondialog.py b/openlp/core/ui/exceptiondialog.py
index 212fee4cd..329ed7797 100644
--- a/openlp/core/ui/exceptiondialog.py
+++ b/openlp/core/ui/exceptiondialog.py
@@ -30,9 +30,9 @@
The GUI widgets of the exception dialog.
"""
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtGui
-from openlp.core.lib import translate
+from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button, create_button_box
@@ -45,6 +45,7 @@ class Ui_ExceptionDialog(object):
Set up the UI.
"""
exception_dialog.setObjectName('exception_dialog')
+ exception_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.exception_layout = QtGui.QVBoxLayout(exception_dialog)
self.exception_layout.setObjectName('exception_layout')
self.message_layout = QtGui.QHBoxLayout()
diff --git a/openlp/core/ui/filerenamedialog.py b/openlp/core/ui/filerenamedialog.py
index 0900b82c3..4a316aece 100644
--- a/openlp/core/ui/filerenamedialog.py
+++ b/openlp/core/ui/filerenamedialog.py
@@ -31,7 +31,7 @@ The UI widgets for the rename dialog
"""
from PyQt4 import QtCore, QtGui
-from openlp.core.lib import translate
+from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +44,7 @@ class Ui_FileRenameDialog(object):
Set up the UI
"""
file_rename_dialog.setObjectName('file_rename_dialog')
+ file_rename_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
file_rename_dialog.resize(300, 10)
self.dialog_layout = QtGui.QGridLayout(file_rename_dialog)
self.dialog_layout.setObjectName('dialog_layout')
diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py
index f1b875e6b..531c4b49f 100644
--- a/openlp/core/ui/firsttimeform.py
+++ b/openlp/core/ui/firsttimeform.py
@@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
Run the wizard.
"""
- self.setDefaults()
+ self.set_defaults()
return QtGui.QWizard.exec_(self)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@@ -199,8 +199,8 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
self.no_internet_label.setText(self.no_internet_text + self.cancelWizardText)
elif page_id == FirstTimePage.Defaults:
self.theme_combo_box.clear()
- for iter in range(self.themes_list_widget.count()):
- item = self.themes_list_widget.item(iter)
+ for index in range(self.themes_list_widget.count()):
+ item = self.themes_list_widget.item(index)
if item.checkState() == QtCore.Qt.Checked:
self.theme_combo_box.addItem(item.text())
if self.has_run_wizard:
@@ -292,13 +292,9 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
themes = self.config.get('themes', 'files')
themes = themes.split(',')
- for theme in themes:
- filename = self.config.get('theme_%s' % theme, 'filename')
+ for index, theme in enumerate(themes):
screenshot = self.config.get('theme_%s' % theme, 'screenshot')
- for index in range(self.themes_list_widget.count()):
- item = self.themes_list_widget.item(index)
- if item.data(QtCore.Qt.UserRole) == filename:
- break
+ item = self.themes_list_widget.item(index)
item.setIcon(build_icon(os.path.join(gettempdir(), 'openlp', screenshot)))
def _get_file_size(self, url):
diff --git a/openlp/core/ui/firsttimelanguagedialog.py b/openlp/core/ui/firsttimelanguagedialog.py
index 10b341c2c..b4263e498 100644
--- a/openlp/core/ui/firsttimelanguagedialog.py
+++ b/openlp/core/ui/firsttimelanguagedialog.py
@@ -32,6 +32,7 @@ The UI widgets of the language selection dialog.
from PyQt4 import QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +45,7 @@ class Ui_FirstTimeLanguageDialog(object):
Set up the UI.
"""
language_dialog.setObjectName('language_dialog')
+ language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
language_dialog.resize(300, 50)
self.dialog_layout = QtGui.QVBoxLayout(language_dialog)
self.dialog_layout.setContentsMargins(8, 8, 8, 8)
diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py
index 1a270f931..35623ded2 100644
--- a/openlp/core/ui/firsttimewizard.py
+++ b/openlp/core/ui/firsttimewizard.py
@@ -34,6 +34,7 @@ from PyQt4 import QtCore, QtGui
import sys
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page
@@ -60,6 +61,7 @@ class Ui_FirstTimeWizard(object):
Set up the UI.
"""
first_time_wizard.setObjectName('first_time_wizard')
+ first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
first_time_wizard.resize(550, 386)
first_time_wizard.setModal(True)
first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle)
diff --git a/openlp/core/ui/formattingtagdialog.py b/openlp/core/ui/formattingtagdialog.py
index 387bca0a7..569405a05 100644
--- a/openlp/core/ui/formattingtagdialog.py
+++ b/openlp/core/ui/formattingtagdialog.py
@@ -45,6 +45,7 @@ class Ui_FormattingTagDialog(object):
Set up the UI
"""
formatting_tag_dialog.setObjectName('formatting_tag_dialog')
+ formatting_tag_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
formatting_tag_dialog.resize(725, 548)
self.list_data_grid_layout = QtGui.QVBoxLayout(formatting_tag_dialog)
self.list_data_grid_layout.setMargin(8)
diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py
index 81e822c16..664c1a596 100644
--- a/openlp/core/ui/mainwindow.py
+++ b/openlp/core/ui/mainwindow.py
@@ -89,7 +89,7 @@ class Ui_MainWindow(object):
Set up the user interface
"""
main_window.setObjectName('MainWindow')
- main_window.setWindowIcon(build_icon(':/icon/openlp-logo-64x64.png'))
+ main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg'))
main_window.setDockNestingEnabled(True)
# Set up the main container, which contains all the other form widgets.
self.main_content = QtGui.QWidget(main_window)
@@ -320,14 +320,14 @@ class Ui_MainWindow(object):
# i18n add Language Actions
add_actions(self.settings_language_menu, (self.auto_language_item, None))
add_actions(self.settings_language_menu, self.language_group.actions())
- # Order things differently in OS X so that Preferences menu item in the
- # app menu is correct (this gets picked up automatically by Qt).
+ # Qt on OS X looks for keywords in the menu items title to determine which menu items get added to the main
+ # menu. If we are running on Mac OS X the menu items whose title contains those keywords but don't belong in the
+ # main menu need to be marked as such with QAction.NoRole.
if sys.platform == 'darwin':
- add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
- None, self.settings_configure_item, self.settings_shortcuts_item, self.formatting_tag_item))
- else:
- add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
- None, self.formatting_tag_item, self.settings_shortcuts_item, self.settings_configure_item))
+ self.settings_shortcuts_item.setMenuRole(QtGui.QAction.NoRole)
+ self.formatting_tag_item.setMenuRole(QtGui.QAction.NoRole)
+ add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
+ None, self.formatting_tag_item, self.settings_shortcuts_item, self.settings_configure_item))
add_actions(self.tools_menu, (self.tools_add_tool_item, None))
add_actions(self.tools_menu, (self.tools_open_data_folder, None))
add_actions(self.tools_menu, (self.tools_first_time_wizard, None))
@@ -1334,7 +1334,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
if self.copy_data:
log.info('Copying data to new path')
try:
- self.showStatusMessage(
+ self.show_status_message(
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
'- Please wait for copy to finish').replace('%s', self.new_data_path))
dir_util.copy_tree(old_data_path, self.new_data_path)
@@ -1364,8 +1364,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
args = []
for a in self.arguments:
args.extend([a])
- for arg in args:
- filename = arg
+ for filename in args:
if not isinstance(filename, str):
filename = str(filename, sys.getfilesystemencoding())
if filename.endswith(('.osz', '.oszl')):
diff --git a/openlp/core/ui/media/mediaplayer.py b/openlp/core/ui/media/mediaplayer.py
index 3246d58c4..22ea7ecfc 100644
--- a/openlp/core/ui/media/mediaplayer.py
+++ b/openlp/core/ui/media/mediaplayer.py
@@ -29,8 +29,6 @@
"""
The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class.
"""
-import os
-
from openlp.core.common import RegistryProperties
from openlp.core.ui.media import MediaState
diff --git a/openlp/core/ui/media/phononplayer.py b/openlp/core/ui/media/phononplayer.py
index 5e94dbd0e..b343755a0 100644
--- a/openlp/core/ui/media/phononplayer.py
+++ b/openlp/core/ui/media/phononplayer.py
@@ -33,10 +33,8 @@ import logging
import mimetypes
from datetime import datetime
-from PyQt4 import QtGui
from PyQt4.phonon import Phonon
-from openlp.core.common import Settings
from openlp.core.lib import translate
from openlp.core.ui.media import MediaState
diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py
index 9f060a2dc..6fd9bafd3 100644
--- a/openlp/core/ui/media/vlcplayer.py
+++ b/openlp/core/ui/media/vlcplayer.py
@@ -34,6 +34,7 @@ from distutils.version import LooseVersion
import logging
import os
import sys
+import threading
from PyQt4 import QtGui
@@ -206,7 +207,9 @@ class VlcPlayer(MediaPlayer):
controller = display.controller
start_time = 0
log.debug('vlc play')
- display.vlc_media_player.play()
+ if self.state != MediaState.Paused and controller.media_info.start_time > 0:
+ start_time = controller.media_info.start_time
+ threading.Thread(target=display.vlc_media_player.play).start()
if not self.media_state_wait(display, vlc.State.Playing):
return False
if self.state != MediaState.Paused and controller.media_info.start_time > 0:
@@ -254,7 +257,7 @@ class VlcPlayer(MediaPlayer):
"""
Stop the current item
"""
- display.vlc_media_player.stop()
+ threading.Thread(target=display.vlc_media_player.stop).start()
self.state = MediaState.Stopped
def volume(self, display, vol):
diff --git a/openlp/core/ui/plugindialog.py b/openlp/core/ui/plugindialog.py
index f4d2d991b..40311677e 100644
--- a/openlp/core/ui/plugindialog.py
+++ b/openlp/core/ui/plugindialog.py
@@ -32,6 +32,7 @@ The UI widgets of the plugin view dialog
from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +45,7 @@ class Ui_PluginViewDialog(object):
Set up the UI
"""
pluginViewDialog.setObjectName('pluginViewDialog')
+ pluginViewDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
pluginViewDialog.setWindowModality(QtCore.Qt.ApplicationModal)
self.plugin_layout = QtGui.QVBoxLayout(pluginViewDialog)
self.plugin_layout.setObjectName('plugin_layout')
diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py
index 91b98b97a..78bdee4a5 100644
--- a/openlp/core/ui/pluginform.py
+++ b/openlp/core/ui/pluginform.py
@@ -30,7 +30,6 @@
The actual plugin view form
"""
import logging
-import os
from PyQt4 import QtGui
diff --git a/openlp/core/ui/printservicedialog.py b/openlp/core/ui/printservicedialog.py
index f59dcb044..0873a0b4a 100644
--- a/openlp/core/ui/printservicedialog.py
+++ b/openlp/core/ui/printservicedialog.py
@@ -56,6 +56,7 @@ class Ui_PrintServiceDialog(object):
Set up the UI
"""
print_service_dialog.setObjectName('print_service_dialog')
+ print_service_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
print_service_dialog.resize(664, 594)
self.main_layout = QtGui.QVBoxLayout(print_service_dialog)
self.main_layout.setSpacing(0)
diff --git a/openlp/core/ui/printserviceform.py b/openlp/core/ui/printserviceform.py
index 489eefa78..41caaab52 100644
--- a/openlp/core/ui/printserviceform.py
+++ b/openlp/core/ui/printserviceform.py
@@ -242,7 +242,7 @@ class PrintServiceForm(QtGui.QDialog, Ui_PrintServiceDialog, RegistryProperties)
Creates a html element. If ``text`` is given, the element's text will set and if a ``parent`` is given,
the element is appended.
- :param tag: The html tag, e. g. ``u'span'``. Defaults to ``None``.
+ :param tag: The html tag, e. g. ``'span'``. Defaults to ``None``.
:param text: The text for the tag. Defaults to ``None``.
:param parent: The parent element. Defaults to ``None``.
:param classId: Value for the class attribute
diff --git a/openlp/core/ui/serviceitemeditdialog.py b/openlp/core/ui/serviceitemeditdialog.py
index 76139c875..1fa19eb31 100644
--- a/openlp/core/ui/serviceitemeditdialog.py
+++ b/openlp/core/ui/serviceitemeditdialog.py
@@ -32,6 +32,7 @@ The UI widgets for the service item edit dialog
from PyQt4 import QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box, create_button
@@ -44,6 +45,7 @@ class Ui_ServiceItemEditDialog(object):
Set up the UI
"""
serviceItemEditDialog.setObjectName('serviceItemEditDialog')
+ serviceItemEditDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.dialog_layout = QtGui.QGridLayout(serviceItemEditDialog)
self.dialog_layout.setContentsMargins(8, 8, 8, 8)
self.dialog_layout.setSpacing(8)
diff --git a/openlp/core/ui/settingsdialog.py b/openlp/core/ui/settingsdialog.py
index f625680d6..fbac6a155 100644
--- a/openlp/core/ui/settingsdialog.py
+++ b/openlp/core/ui/settingsdialog.py
@@ -45,8 +45,8 @@ class Ui_SettingsDialog(object):
Set up the UI
"""
settings_dialog.setObjectName('settings_dialog')
+ settings_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
settings_dialog.resize(800, 500)
- settings_dialog.setWindowIcon(build_icon(':/system/system_settings.png'))
self.dialog_layout = QtGui.QGridLayout(settings_dialog)
self.dialog_layout.setObjectName('dialog_layout')
self.dialog_layout.setMargin(8)
diff --git a/openlp/core/ui/shortcutlistdialog.py b/openlp/core/ui/shortcutlistdialog.py
index 54433da24..ef2dc4056 100644
--- a/openlp/core/ui/shortcutlistdialog.py
+++ b/openlp/core/ui/shortcutlistdialog.py
@@ -66,6 +66,7 @@ class Ui_ShortcutListDialog(object):
Set up the UI
"""
shortcutListDialog.setObjectName('shortcutListDialog')
+ shortcutListDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
shortcutListDialog.resize(500, 438)
self.shortcut_list_layout = QtGui.QVBoxLayout(shortcutListDialog)
self.shortcut_list_layout.setObjectName('shortcut_list_layout')
diff --git a/openlp/core/ui/shortcutlistform.py b/openlp/core/ui/shortcutlistform.py
index 4b64c3b54..dbfbbd439 100644
--- a/openlp/core/ui/shortcutlistform.py
+++ b/openlp/core/ui/shortcutlistform.py
@@ -244,10 +244,10 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
self.primary_push_button.setChecked(False)
self.alternate_push_button.setChecked(False)
else:
- if action.defaultShortcuts:
- primary_label_text = action.defaultShortcuts[0].toString()
- if len(action.defaultShortcuts) == 2:
- alternate_label_text = action.defaultShortcuts[1].toString()
+ if action.default_shortcuts:
+ primary_label_text = action.default_shortcuts[0].toString()
+ if len(action.default_shortcuts) == 2:
+ alternate_label_text = action.default_shortcuts[1].toString()
shortcuts = self._action_shortcuts(action)
# We do not want to loose pending changes, that is why we have to keep the text when, this function has not
# been triggered by a signal.
@@ -292,7 +292,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
self._adjust_button(self.alternate_push_button, False, text='')
for category in self.action_list.categories:
for action in category.actions:
- self.changed_actions[action] = action.defaultShortcuts
+ self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
def on_default_radio_button_clicked(self, toggled):
@@ -306,7 +306,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
if action is None:
return
temp_shortcuts = self._action_shortcuts(action)
- self.changed_actions[action] = action.defaultShortcuts
+ self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
primary_button_text = ''
alternate_button_text = ''
@@ -357,8 +357,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = []
- if action.defaultShortcuts:
- new_shortcuts.append(action.defaultShortcuts[0])
+ if action.default_shortcuts:
+ new_shortcuts.append(action.default_shortcuts[0])
# We have to check if the primary default shortcut is available. But we only have to check, if the action
# has a default primary shortcut (an "empty" shortcut is always valid and if the action does not have a
# default primary shortcut, then the alternative shortcut (not the default one) will become primary
@@ -383,8 +383,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
new_shortcuts = []
if shortcuts:
new_shortcuts.append(shortcuts[0])
- if len(action.defaultShortcuts) == 2:
- new_shortcuts.append(action.defaultShortcuts[1])
+ if len(action.default_shortcuts) == 2:
+ new_shortcuts.append(action.default_shortcuts[1])
if len(new_shortcuts) == 2:
if not self._validiate_shortcut(action, new_shortcuts[1]):
return
diff --git a/openlp/core/ui/starttimedialog.py b/openlp/core/ui/starttimedialog.py
index 54e0629a4..cfd507bb2 100644
--- a/openlp/core/ui/starttimedialog.py
+++ b/openlp/core/ui/starttimedialog.py
@@ -32,6 +32,7 @@ The UI widgets for the time dialog
from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +45,7 @@ class Ui_StartTimeDialog(object):
Set up the UI
"""
StartTimeDialog.setObjectName('StartTimeDialog')
+ StartTimeDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
StartTimeDialog.resize(350, 10)
self.dialog_layout = QtGui.QGridLayout(StartTimeDialog)
self.dialog_layout.setObjectName('dialog_layout')
diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py
index d9b61e117..321071c49 100644
--- a/openlp/core/ui/themeform.py
+++ b/openlp/core/ui/themeform.py
@@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
self.footer_font_combo_box.activated.connect(self.update_theme)
self.footer_size_spin_box.valueChanged.connect(self.update_theme)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@@ -261,7 +261,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
log.debug('Editing theme %s' % self.theme.theme_name)
self.temp_background_filename = ''
self.update_theme_allowed = False
- self.setDefaults()
+ self.set_defaults()
self.update_theme_allowed = True
self.theme_name_label.setVisible(not edit)
self.theme_name_edit.setVisible(not edit)
diff --git a/openlp/core/ui/themelayoutdialog.py b/openlp/core/ui/themelayoutdialog.py
index 023d6259c..d3fbeed2d 100644
--- a/openlp/core/ui/themelayoutdialog.py
+++ b/openlp/core/ui/themelayoutdialog.py
@@ -32,6 +32,7 @@ The layout of the theme
from PyQt4 import QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +45,7 @@ class Ui_ThemeLayoutDialog(object):
Set up the UI
"""
themeLayoutDialog.setObjectName('themeLayoutDialogDialog')
+ themeLayoutDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.preview_layout = QtGui.QVBoxLayout(themeLayoutDialog)
self.preview_layout.setObjectName('preview_layout')
self.preview_area = QtGui.QWidget(themeLayoutDialog)
diff --git a/openlp/core/ui/themestab.py b/openlp/core/ui/themestab.py
index 1f54f984b..0478f0ed0 100644
--- a/openlp/core/ui/themestab.py
+++ b/openlp/core/ui/themestab.py
@@ -190,7 +190,7 @@ class ThemesTab(SettingsTab):
:param theme_list: The list of available themes::
- [u'Bible Theme', u'Song Theme']
+ ['Bible Theme', 'Song Theme']
"""
# Reload as may have been triggered by the ThemeManager.
self.global_theme = Settings().value(self.settings_section + '/global theme')
diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py
index 77ccb0663..bda52c807 100644
--- a/openlp/core/ui/themewizard.py
+++ b/openlp/core/ui/themewizard.py
@@ -46,6 +46,7 @@ class Ui_ThemeWizard(object):
Set up the UI
"""
themeWizard.setObjectName('OpenLP.ThemeWizard')
+ themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
themeWizard.setModal(True)
themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle)
themeWizard.setOptions(QtGui.QWizard.IndependentPages |
diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py
index 05951d14a..908981dc6 100644
--- a/openlp/core/ui/wizard.py
+++ b/openlp/core/ui/wizard.py
@@ -118,6 +118,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
"""
Set up the wizard UI.
"""
+ self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.setModal(True)
self.setWizardStyle(QtGui.QWizard.ModernStyle)
self.setOptions(QtGui.QWizard.IndependentPages |
@@ -197,7 +198,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
"""
Run the wizard.
"""
- self.setDefaults()
+ self.set_defaults()
return QtGui.QWizard.exec_(self)
def reject(self):
@@ -279,7 +280,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
:param filters: The file extension filters. It should contain the file description
as well as the file extension. For example::
- u'OpenLP 2.0 Databases (*.sqlite)'
+ 'OpenLP 2.0 Databases (*.sqlite)'
"""
if filters:
filters += ';;'
diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py
index a5b5f356a..add663132 100644
--- a/openlp/core/utils/__init__.py
+++ b/openlp/core/utils/__init__.py
@@ -113,7 +113,7 @@ def get_application_version():
"""
Returns the application version of the running instance of OpenLP::
- {u'full': u'1.9.4-bzr1249', u'version': u'1.9.4', u'build': u'bzr1249'}
+ {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
"""
global APPLICATION_VERSION
if APPLICATION_VERSION:
diff --git a/openlp/core/utils/actions.py b/openlp/core/utils/actions.py
index 29f2d279b..d81e16b2e 100644
--- a/openlp/core/utils/actions.py
+++ b/openlp/core/utils/actions.py
@@ -65,20 +65,14 @@ class CategoryActionList(object):
self.index = 0
self.actions = []
- def __getitem__(self, key):
- """
- Implement the __getitem__() method to make this class a dictionary type
- """
- for weight, action in self.actions:
- if action.text() == key:
- return action
- raise KeyError('Action "%s" does not exist.' % key)
-
- def __contains__(self, item):
+ def __contains__(self, key):
"""
Implement the __contains__() method to make this class a dictionary type
"""
- return item in self
+ for weight, action in self.actions:
+ if action == key:
+ return True
+ return False
def __len__(self):
"""
@@ -103,23 +97,14 @@ class CategoryActionList(object):
self.index += 1
return self.actions[self.index - 1][1]
- def has_key(self, key):
- """
- Implement the has_key() method to make this class a dictionary type
- """
- for weight, action in self.actions:
- if action.text() == key:
- return True
- return False
-
- def append(self, name):
+ def append(self, action):
"""
Append an action
"""
weight = 0
if self.actions:
weight = self.actions[-1][0] + 1
- self.add(name, weight)
+ self.add(action, weight)
def add(self, action, weight=0):
"""
@@ -128,14 +113,15 @@ class CategoryActionList(object):
self.actions.append((weight, action))
self.actions.sort(key=lambda act: act[0])
- def remove(self, remove_action):
+ def remove(self, action):
"""
Remove an action
"""
- for action in self.actions:
- if action[1] == remove_action:
- self.actions.remove(action)
+ for item in self.actions:
+ if item[1] == action:
+ self.actions.remove(item)
return
+ raise ValueError('Action "%s" does not exist.' % action)
class CategoryList(object):
@@ -184,9 +170,9 @@ class CategoryList(object):
self.index += 1
return self.categories[self.index - 1]
- def has_key(self, key):
+ def __contains__(self, key):
"""
- Implement the has_key() method to make this class like a dictionary
+ Implement the __contains__() method to make this class like a dictionary
"""
for category in self.categories:
if category.name == key:
@@ -200,10 +186,7 @@ class CategoryList(object):
weight = 0
if self.categories:
weight = self.categories[-1].weight + 1
- if actions:
- self.add(name, weight, actions)
- else:
- self.add(name, weight)
+ self.add(name, weight, actions)
def add(self, name, weight=0, actions=None):
"""
@@ -226,6 +209,8 @@ class CategoryList(object):
for category in self.categories:
if category.name == name:
self.categories.remove(category)
+ return
+ raise ValueError('Category "%s" does not exist.' % name)
class ActionList(object):
@@ -270,7 +255,7 @@ class ActionList(object):
settings = Settings()
settings.beginGroup('shortcuts')
# Get the default shortcut from the config.
- action.defaultShortcuts = settings.get_default_value(action.objectName())
+ action.default_shortcuts = settings.get_default_value(action.objectName())
if weight is None:
self.categories[category].actions.append(action)
else:
diff --git a/openlp/plugins/alerts/forms/__init__.py b/openlp/plugins/alerts/forms/__init__.py
index 55d44372e..fbe3c7170 100644
--- a/openlp/plugins/alerts/forms/__init__.py
+++ b/openlp/plugins/alerts/forms/__init__.py
@@ -32,7 +32,7 @@ other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
-converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
+converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned
diff --git a/openlp/plugins/alerts/forms/alertdialog.py b/openlp/plugins/alerts/forms/alertdialog.py
index e4fd29a39..47e61365a 100644
--- a/openlp/plugins/alerts/forms/alertdialog.py
+++ b/openlp/plugins/alerts/forms/alertdialog.py
@@ -46,7 +46,7 @@ class Ui_AlertDialog(object):
"""
alert_dialog.setObjectName('alert_dialog')
alert_dialog.resize(400, 300)
- alert_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
+ alert_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.alert_dialog_layout = QtGui.QGridLayout(alert_dialog)
self.alert_dialog_layout.setObjectName('alert_dialog_layout')
self.alert_text_layout = QtGui.QFormLayout()
diff --git a/openlp/plugins/bibles/forms/__init__.py b/openlp/plugins/bibles/forms/__init__.py
index 974d6d7a5..d6c77a9a3 100644
--- a/openlp/plugins/bibles/forms/__init__.py
+++ b/openlp/plugins/bibles/forms/__init__.py
@@ -33,7 +33,7 @@ other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
-converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
+converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned
diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py
index ee5bee2d0..79b0bc699 100644
--- a/openlp/plugins/bibles/forms/bibleimportform.py
+++ b/openlp/plugins/bibles/forms/bibleimportform.py
@@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard):
self.license_details_page.registerField('license_copyright', self.copyright_edit)
self.license_details_page.registerField('license_permissions', self.permissions_edit)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default values for the wizard pages.
"""
diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py
index d9936dfe6..09c0942b7 100644
--- a/openlp/plugins/bibles/forms/bibleupgradeform.py
+++ b/openlp/plugins/bibles/forms/bibleupgradeform.py
@@ -307,7 +307,7 @@ class BibleUpgradeForm(OpenLPWizard):
if self.currentPage() == self.progress_page:
return True
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default values for the wizard pages.
"""
diff --git a/openlp/plugins/bibles/forms/booknamedialog.py b/openlp/plugins/bibles/forms/booknamedialog.py
index 5903391c3..120caeff0 100644
--- a/openlp/plugins/bibles/forms/booknamedialog.py
+++ b/openlp/plugins/bibles/forms/booknamedialog.py
@@ -30,12 +30,14 @@
from PyQt4 import QtCore, QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
class Ui_BookNameDialog(object):
def setupUi(self, book_name_dialog):
book_name_dialog.setObjectName('book_name_dialog')
+ book_name_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
book_name_dialog.resize(400, 271)
self.book_name_layout = QtGui.QVBoxLayout(book_name_dialog)
self.book_name_layout.setSpacing(8)
diff --git a/openlp/plugins/bibles/forms/editbibledialog.py b/openlp/plugins/bibles/forms/editbibledialog.py
index 1fbaa2f1e..e9e39a053 100644
--- a/openlp/plugins/bibles/forms/editbibledialog.py
+++ b/openlp/plugins/bibles/forms/editbibledialog.py
@@ -39,8 +39,8 @@ from openlp.plugins.bibles.lib.db import BiblesResourcesDB
class Ui_EditBibleDialog(object):
def setupUi(self, edit_bible_dialog):
edit_bible_dialog.setObjectName('edit_bible_dialog')
+ edit_bible_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_bible_dialog.resize(520, 400)
- edit_bible_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
edit_bible_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_bible_dialog)
self.dialog_layout.setSpacing(8)
diff --git a/openlp/plugins/bibles/forms/languagedialog.py b/openlp/plugins/bibles/forms/languagedialog.py
index 10382ea13..ab40503d2 100644
--- a/openlp/plugins/bibles/forms/languagedialog.py
+++ b/openlp/plugins/bibles/forms/languagedialog.py
@@ -30,12 +30,14 @@
from PyQt4 import QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
class Ui_LanguageDialog(object):
def setupUi(self, language_dialog):
language_dialog.setObjectName('language_dialog')
+ language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
language_dialog.resize(400, 165)
self.language_layout = QtGui.QVBoxLayout(language_dialog)
self.language_layout.setSpacing(8)
diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py
index 50a0e2a63..d67319797 100644
--- a/openlp/plugins/bibles/lib/__init__.py
+++ b/openlp/plugins/bibles/lib/__init__.py
@@ -262,7 +262,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
For example::
- [(u'John', 3, 16, 18), (u'John', 4, 1, 1)]
+ [('John', 3, 16, 18), ('John', 4, 1, 1)]
**Reference string details:**
@@ -311,7 +311,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
``(?P[0-9]+)``
The ``to_verse`` reference is equivalent to group 2.
- The full reference is matched against get_reference_match(u'full'). This regular expression looks like this:
+ The full reference is matched against get_reference_match('full'). This regular expression looks like this:
``^\s*(?!\s)(?P[\d]*[^\d]+)(?Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
-converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
+converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned
diff --git a/openlp/plugins/images/lib/db.py b/openlp/plugins/images/lib/db.py
index 68ca3d11d..896f93b17 100644
--- a/openlp/plugins/images/lib/db.py
+++ b/openlp/plugins/images/lib/db.py
@@ -31,7 +31,7 @@ The :mod:`db` module provides the database and schema that is the backend for th
"""
from sqlalchemy import Column, ForeignKey, Table, types
-from sqlalchemy.orm import mapper, relation, reconstructor
+from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db
diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py
index c28f1e834..36df55dac 100644
--- a/openlp/plugins/images/lib/mediaitem.py
+++ b/openlp/plugins/images/lib/mediaitem.py
@@ -353,7 +353,7 @@ class ImageMediaItem(MediaManagerItem):
icon = build_icon(thumb)
else:
icon = create_thumb(imageFile.filename, thumb)
- item_name = QtGui.QTreeWidgetItem(filename)
+ item_name = QtGui.QTreeWidgetItem([filename])
item_name.setText(0, filename)
item_name.setIcon(0, icon)
item_name.setToolTip(0, imageFile.filename)
diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py
index c5510b7e4..626ef36c9 100644
--- a/openlp/plugins/media/lib/mediaitem.py
+++ b/openlp/plugins/media/lib/mediaitem.py
@@ -379,7 +379,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
"""
media = Settings().value(self.settings_section + '/media files')
media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1]))
- extension = []
if type == MediaType.Audio:
extension = self.media_controller.audio_extensions_list
else:
diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py
index 6c8d7fb8c..3060bcdb0 100644
--- a/openlp/plugins/presentations/lib/presentationcontroller.py
+++ b/openlp/plugins/presentations/lib/presentationcontroller.py
@@ -354,7 +354,7 @@ class PresentationController(object):
class MyPresentationController(PresentationController):
def __init__(self, plugin):
PresentationController.__init(
- self, plugin, u'My Presenter App')
+ self, plugin, 'My Presenter App')
:param plugin: Defaults to *None*. The presentationplugin object
:param name: Name of the application, to appear in the application
diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py
index 5a10a14ae..4241b34dc 100644
--- a/openlp/plugins/remotes/lib/httprouter.py
+++ b/openlp/plugins/remotes/lib/httprouter.py
@@ -149,11 +149,11 @@ class HttpRouter(RegistryProperties):
"""
Initialise the router stack and any other variables.
"""
- authcode = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
+ auth_code = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
try:
- self.auth = base64.b64encode(authcode)
+ self.auth = base64.b64encode(auth_code)
except TypeError:
- self.auth = base64.b64encode(authcode.encode()).decode()
+ self.auth = base64.b64encode(auth_code.encode()).decode()
self.routes = [
('^/$', {'function': self.serve_file, 'secure': False}),
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
@@ -376,7 +376,6 @@ class HttpRouter(RegistryProperties):
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
Returns the extension and the content_type
"""
- content_type = 'text/plain'
ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type
@@ -439,7 +438,7 @@ class HttpRouter(RegistryProperties):
if plugin.status == PluginStatus.Active:
try:
text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
text = urllib.parse.unquote(text)
self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text])
@@ -453,6 +452,7 @@ class HttpRouter(RegistryProperties):
"""
Perform an action on the slide controller.
"""
+ log.debug("controller_text var = %s" % var)
current_item = self.live_controller.service_item
data = []
if current_item:
@@ -488,7 +488,7 @@ class HttpRouter(RegistryProperties):
if self.request_data:
try:
data = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
log.info(data)
# This slot expects an int within a list.
@@ -547,7 +547,7 @@ class HttpRouter(RegistryProperties):
"""
try:
text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
text = urllib.parse.unquote(text)
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
@@ -563,12 +563,12 @@ class HttpRouter(RegistryProperties):
Go live on an item of type ``plugin``.
"""
try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ request_id = json.loads(self.request_data)['request']['id']
+ except KeyError:
return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
- plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [id, True])
+ plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [request_id, True])
return self.do_http_success()
def add_to_service(self, plugin_name):
@@ -576,11 +576,11 @@ class HttpRouter(RegistryProperties):
Add item of type ``plugin_name`` to the end of the service.
"""
try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ request_id = json.loads(self.request_data)['request']['id']
+ except KeyError:
return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
- item_id = plugin.media_item.create_item_from_id(id)
+ item_id = plugin.media_item.create_item_from_id(request_id)
plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
self.do_http_success()
diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py
index 22d0349f8..9a904090d 100644
--- a/openlp/plugins/remotes/lib/httpserver.py
+++ b/openlp/plugins/remotes/lib/httpserver.py
@@ -40,7 +40,7 @@ import time
from PyQt4 import QtCore
-from openlp.core.common import AppLocation, Settings
+from openlp.core.common import AppLocation, Settings, RegistryProperties
from openlp.plugins.remotes.lib import HttpRouter
@@ -94,13 +94,18 @@ class HttpThread(QtCore.QThread):
"""
self.http_server.start_server()
+ def stop(self):
+ log.debug("stop called")
+ self.http_server.stop = True
-class OpenLPServer():
+
+class OpenLPServer(RegistryProperties):
def __init__(self):
"""
Initialise the http server, and start the server of the correct type http / https
"""
- log.debug('Initialise httpserver')
+ super(OpenLPServer, self).__init__()
+ log.debug('Initialise OpenLP')
self.settings_section = 'remotes'
self.http_thread = HttpThread(self)
self.http_thread.start()
@@ -110,32 +115,49 @@ class OpenLPServer():
Start the correct server and save the handler
"""
address = Settings().value(self.settings_section + '/ip address')
- if Settings().value(self.settings_section + '/https enabled'):
+ self.address = address
+ self.is_secure = Settings().value(self.settings_section + '/https enabled')
+ self.needs_authentication = Settings().value(self.settings_section + '/authentication enabled')
+ if self.is_secure:
port = Settings().value(self.settings_section + '/https port')
- self.httpd = HTTPSServer((address, port), CustomHandler)
- log.debug('Started ssl httpd...')
+ self.port = port
+ self.start_server_instance(address, port, HTTPSServer)
else:
port = Settings().value(self.settings_section + '/port')
- loop = 1
- while loop < 3:
- try:
- self.httpd = ThreadingHTTPServer((address, port), CustomHandler)
- except OSError:
- loop += 1
- time.sleep(0.1)
- except:
- log.error('Failed to start server ')
- log.debug('Started non ssl httpd...')
+ self.port = port
+ self.start_server_instance(address, port, ThreadingHTTPServer)
if hasattr(self, 'httpd') and self.httpd:
self.httpd.serve_forever()
else:
log.debug('Failed to start server')
+ def start_server_instance(self, address, port, server_class):
+ """
+ Start the server
+
+ :param address: The server address
+ :param port: The run port
+ :param server_class: the class to start
+ """
+ loop = 1
+ while loop < 4:
+ try:
+ self.httpd = server_class((address, port), CustomHandler)
+ log.debug("Server started for class %s %s %d" % (server_class, address, port))
+ except OSError:
+ log.debug("failed to start http server thread state %d %s" %
+ (loop, self.http_thread.isRunning()))
+ loop += 1
+ time.sleep(0.1)
+ except:
+ log.error('Failed to start server ')
+
def stop_server(self):
"""
Stop the server
"""
- self.http_thread.exit(0)
+ if self.http_thread.isRunning():
+ self.http_thread.stop()
self.httpd = None
log.debug('Stopped the server.')
diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py
index d6b96cc1c..4db25cfc2 100644
--- a/openlp/plugins/remotes/lib/remotetab.py
+++ b/openlp/plugins/remotes/lib/remotetab.py
@@ -32,7 +32,7 @@ import os.path
from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.common import AppLocation, Settings, translate
-from openlp.core.lib import SettingsTab
+from openlp.core.lib import SettingsTab, build_icon
ZERO_URL = '0.0.0.0'
@@ -234,6 +234,7 @@ class RemoteTab(SettingsTab):
"""
Load the configuration and update the server configuration if necessary
"""
+ self.is_secure = Settings().value(self.settings_section + '/https enabled')
self.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + '/https port'))
self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
@@ -263,9 +264,7 @@ class RemoteTab(SettingsTab):
Settings().value(self.settings_section + '/port') != self.port_spin_box.value() or \
Settings().value(self.settings_section + '/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + '/https enabled') != \
- self.https_settings_group_box.isChecked() or \
- Settings().value(self.settings_section + '/authentication enabled') != \
- self.user_login_group_box.isChecked():
+ self.https_settings_group_box.isChecked():
self.settings_form.register_post_process('remotes_config_updated')
Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + '/https port', self.https_port_spin_box.value())
@@ -275,6 +274,7 @@ class RemoteTab(SettingsTab):
Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text())
+ self.generate_icon()
def on_twelve_hour_check_box_changed(self, check_state):
"""
@@ -290,3 +290,25 @@ class RemoteTab(SettingsTab):
Invert the HTTP group box based on Https group settings
"""
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
+
+ def generate_icon(self):
+ """
+ Generate icon for main window
+ """
+ self.remote_server_icon.hide()
+ icon = QtGui.QImage(':/remote/network_server.png')
+ icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ if self.is_secure:
+ overlay = QtGui.QImage(':/remote/network_ssl.png')
+ overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ painter = QtGui.QPainter(icon)
+ painter.drawImage(0, 0, overlay)
+ painter.end()
+ if Settings().value(self.settings_section + '/authentication enabled'):
+ overlay = QtGui.QImage(':/remote/network_auth.png')
+ overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ painter = QtGui.QPainter(icon)
+ painter.drawImage(20, 0, overlay)
+ painter.end()
+ self.remote_server_icon.setPixmap(QtGui.QPixmap.fromImage(icon))
+ self.remote_server_icon.show()
diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py
index d3dc6e58a..582192df4 100644
--- a/openlp/plugins/remotes/remoteplugin.py
+++ b/openlp/plugins/remotes/remoteplugin.py
@@ -28,7 +28,8 @@
###############################################################################
import logging
-import time
+
+from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
@@ -67,6 +68,21 @@ class RemotesPlugin(Plugin):
log.debug('initialise')
super(RemotesPlugin, self).initialise()
self.server = OpenLPServer()
+ if not hasattr(self, 'remote_server_icon'):
+ self.remote_server_icon = QtGui.QLabel(self.main_window.status_bar)
+ size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+ size_policy.setHorizontalStretch(0)
+ size_policy.setVerticalStretch(0)
+ size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
+ self.remote_server_icon.setSizePolicy(size_policy)
+ self.remote_server_icon.setFrameShadow(QtGui.QFrame.Plain)
+ self.remote_server_icon.setLineWidth(1)
+ self.remote_server_icon.setScaledContents(True)
+ self.remote_server_icon.setFixedSize(20, 20)
+ self.remote_server_icon.setObjectName('remote_server_icon')
+ self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
+ self.settings_tab.remote_server_icon = self.remote_server_icon
+ self.settings_tab.generate_icon()
def finalise(self):
"""
@@ -104,9 +120,11 @@ class RemotesPlugin(Plugin):
def config_update(self):
"""
- Called when Config is changed to restart the server on new address or port
+ Called when Config is changed to requests a restart with the server on new address or port
"""
log.debug('remote config changed')
- self.finalise()
- time.sleep(0.5)
- self.initialise()
+ QtGui.QMessageBox.information(self.main_window,
+ translate('RemotePlugin', 'Server Config Change'),
+ translate('RemotePlugin', 'Server configuration changes will require a restart '
+ 'to take effect.'),
+ QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))
diff --git a/openlp/plugins/songs/forms/__init__.py b/openlp/plugins/songs/forms/__init__.py
index 3094a0eda..eb29c12bf 100644
--- a/openlp/plugins/songs/forms/__init__.py
+++ b/openlp/plugins/songs/forms/__init__.py
@@ -34,7 +34,7 @@ code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named
``Ui_Dialog``. It is a slightly modified version of the class that the
``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
-converting most strings from "" to u'' and using OpenLP's ``translate()``
+converting most strings from "" to '' and using OpenLP's ``translate()``
function for translating strings.
The second class, commonly known as the **Form** class, is typically named
diff --git a/openlp/plugins/songs/forms/authorsdialog.py b/openlp/plugins/songs/forms/authorsdialog.py
index 65fb169b0..7bca76ca6 100644
--- a/openlp/plugins/songs/forms/authorsdialog.py
+++ b/openlp/plugins/songs/forms/authorsdialog.py
@@ -43,8 +43,8 @@ class Ui_AuthorsDialog(object):
Set up the UI for the dialog.
"""
authors_dialog.setObjectName('authors_dialog')
+ authors_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
authors_dialog.resize(300, 10)
- authors_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
authors_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(authors_dialog)
self.dialog_layout.setObjectName('dialog_layout')
diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py
index c99dee4a7..c411c8c1c 100644
--- a/openlp/plugins/songs/forms/duplicatesongremovalform.py
+++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py
@@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True
self.plugin.media_item.on_search_text_button_clicked()
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default form values for the song import wizard.
"""
diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py
index f2ef5af06..a9ca71946 100644
--- a/openlp/plugins/songs/forms/editsongdialog.py
+++ b/openlp/plugins/songs/forms/editsongdialog.py
@@ -43,8 +43,8 @@ class Ui_EditSongDialog(object):
"""
def setupUi(self, edit_song_dialog):
edit_song_dialog.setObjectName('edit_song_dialog')
+ edit_song_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_song_dialog.resize(650, 400)
- edit_song_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
edit_song_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_song_dialog)
self.dialog_layout.setSpacing(8)
@@ -118,13 +118,18 @@ class Ui_EditSongDialog(object):
self.authors_group_box.setObjectName('authors_group_box')
self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box)
self.authors_layout.setObjectName('authors_layout')
- self.author_add_layout = QtGui.QHBoxLayout()
+ self.author_add_layout = QtGui.QVBoxLayout()
self.author_add_layout.setObjectName('author_add_layout')
+ self.author_type_layout = QtGui.QHBoxLayout()
+ self.author_type_layout.setObjectName('author_type_layout')
self.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box')
self.author_add_layout.addWidget(self.authors_combo_box)
+ self.author_types_combo_box = create_combo_box(self.authors_group_box, 'author_types_combo_box', editable=False)
+ self.author_type_layout.addWidget(self.author_types_combo_box)
self.author_add_button = QtGui.QPushButton(self.authors_group_box)
self.author_add_button.setObjectName('author_add_button')
- self.author_add_layout.addWidget(self.author_add_button)
+ self.author_type_layout.addWidget(self.author_add_button)
+ self.author_add_layout.addLayout(self.author_type_layout)
self.authors_layout.addLayout(self.author_add_layout)
self.authors_list_view = QtGui.QListWidget(self.authors_group_box)
self.authors_list_view.setAlternatingRowColors(True)
@@ -330,7 +335,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', 'Warning: You have not entered a verse order.')
-def create_combo_box(parent, name):
+def create_combo_box(parent, name, editable=True):
"""
Utility method to generate a standard combo box for this dialog.
@@ -340,7 +345,7 @@ def create_combo_box(parent, name):
combo_box = QtGui.QComboBox(parent)
combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
- combo_box.setEditable(True)
+ combo_box.setEditable(editable)
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name)
return combo_box
diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py
index 60c6eae78..ae47a110f 100644
--- a/openlp/plugins/songs/forms/editsongform.py
+++ b/openlp/plugins/songs/forms/editsongform.py
@@ -42,7 +42,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStri
from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list
from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile
+from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorSong, AuthorType, Topic, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
@@ -107,6 +107,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.audio_list_widget.setAlternatingRowColors(True)
self.find_verse_split = re.compile('---\[\]---\n', re.UNICODE)
self.whitespace = re.compile(r'\W+', re.UNICODE)
+ self.find_tags = re.compile(u'\{/?\w+\}', re.UNICODE)
def _load_objects(self, cls, combo, cache):
"""
@@ -122,12 +123,12 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
combo.setItemData(row, obj.id)
set_case_insensitive_completer(cache, combo)
- def _add_author_to_list(self, author):
+ def _add_author_to_list(self, author, author_type):
"""
Add an author to the author list.
"""
- author_item = QtGui.QListWidgetItem(str(author.display_name))
- author_item.setData(QtCore.Qt.UserRole, author.id)
+ author_item = QtGui.QListWidgetItem(author.get_display_name(author_type))
+ author_item.setData(QtCore.Qt.UserRole, (author.id, author_type))
self.authors_list_view.addItem(author_item)
def _extract_verse_order(self, verse_order):
@@ -217,8 +218,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
if self.authors_list_view.count() == 0:
self.song_tab_widget.setCurrentIndex(1)
self.authors_list_view.setFocus()
- critical_error_message_box(
- message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.'))
+ critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
+ 'You need to have an author for this song.'))
return False
if self.verse_order_edit.text():
result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount())
@@ -234,8 +235,57 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.manager.save_object(book)
else:
return False
+ # Validate tags (lp#1199639)
+ misplaced_tags = []
+ verse_tags = []
+ for i in range(self.verse_list_widget.rowCount()):
+ item = self.verse_list_widget.item(i, 0)
+ tags = self.find_tags.findall(item.text())
+ field = item.data(QtCore.Qt.UserRole)
+ verse_tags.append(field)
+ if not self._validate_tags(tags):
+ misplaced_tags.append('%s %s' % (VerseType.translated_name(field[0]), field[1:]))
+ if misplaced_tags:
+ critical_error_message_box(
+ message=translate('SongsPlugin.EditSongForm',
+ 'There are misplaced formatting tags in the following verses:\n\n%s\n\n'
+ 'Please correct these tags before continuing.' % ', '.join(misplaced_tags)))
+ return False
+ for tag in verse_tags:
+ if verse_tags.count(tag) > 26:
+ # lp#1310523: OpenLyrics allows only a-z variants of one verse:
+ # http://openlyrics.info/dataformat.html#verse-name
+ critical_error_message_box(message=translate(
+ 'SongsPlugin.EditSongForm', 'You have %(count)s verses named %(name)s %(number)s. '
+ 'You can have at most 26 verses with the same name' %
+ {'count': verse_tags.count(tag),
+ 'name': VerseType.translated_name(tag[0]),
+ 'number': tag[1:]}))
+ return False
return True
+ def _validate_tags(self, tags):
+ """
+ Validates a list of tags
+ Deletes the first affiliated tag pair which is located side by side in the list
+ and call itself recursively with the shortened tag list.
+ If there is any misplaced tag in the list, either the length of the tag list is not even,
+ or the function won't find any tag pairs side by side.
+ If there is no misplaced tag, the length of the list will be zero on any recursive run.
+
+ :param tags: A list of tags
+ :return: True if the function can't find any mismatched tags. Else False.
+ """
+ if len(tags) == 0:
+ return True
+ if len(tags) % 2 != 0:
+ return False
+ for i in range(len(tags)-1):
+ if tags[i+1] == "{/" + tags[i][1:]:
+ del tags[i:i+2]
+ return self._validate_tags(tags)
+ return False
+
def _process_lyrics(self):
"""
Process the lyric data entered by the user into the OpenLP XML format.
@@ -302,6 +352,15 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.authors.append(author.display_name)
set_case_insensitive_completer(self.authors, self.authors_combo_box)
+ # Types
+ self.author_types_combo_box.clear()
+ self.author_types_combo_box.addItem('')
+ # Don't iterate over the dictionary to give them this specific order
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Words], AuthorType.Words)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Music], AuthorType.Music)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.WordsAndMusic], AuthorType.WordsAndMusic)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Translation], AuthorType.Translation)
+
def load_topics(self):
"""
Load the topics into the combobox.
@@ -454,10 +513,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.tag_rows()
# clear the results
self.authors_list_view.clear()
- for author in self.song.authors:
- author_name = QtGui.QListWidgetItem(str(author.display_name))
- author_name.setData(QtCore.Qt.UserRole, author.id)
- self.authors_list_view.addItem(author_name)
+ for author_song in self.song.authors_songs:
+ self._add_author_to_list(author_song.author, author_song.author_type)
# clear the results
self.topics_list_view.clear()
for topic in self.song.topics:
@@ -496,6 +553,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
"""
item = int(self.authors_combo_box.currentIndex())
text = self.authors_combo_box.currentText().strip(' \r\n\t')
+ author_type = self.author_types_combo_box.itemData(self.author_types_combo_box.currentIndex())
# This if statement is for OS X, which doesn't seem to work well with
# the QCompleter auto-completion class. See bug #812628.
if text in self.authors:
@@ -513,7 +571,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
display_name=text)
self.manager.save_object(author)
- self._add_author_to_list(author)
+ self._add_author_to_list(author, author_type)
self.load_authors()
self.authors_combo_box.setCurrentIndex(0)
else:
@@ -521,11 +579,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0:
item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id)
- if self.authors_list_view.findItems(str(author.display_name), QtCore.Qt.MatchExactly):
+ if self.authors_list_view.findItems(author.get_display_name(author_type), QtCore.Qt.MatchExactly):
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else:
- self._add_author_to_list(author)
+ self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0)
else:
QtGui.QMessageBox.warning(
@@ -905,13 +963,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
else:
self.song.theme_name = None
self._process_lyrics()
- self.song.authors = []
+ self.song.authors_songs = []
for row in range(self.authors_list_view.count()):
item = self.authors_list_view.item(row)
- author_id = (item.data(QtCore.Qt.UserRole))
- author = self.manager.get_object(Author, author_id)
- if author is not None:
- self.song.authors.append(author)
+ author_song = AuthorSong()
+ author_song.author_id = item.data(QtCore.Qt.UserRole)[0]
+ author_song.author_type = item.data(QtCore.Qt.UserRole)[1]
+ self.song.authors_songs.append(author_song)
self.song.topics = []
for row in range(self.topics_list_view.count()):
item = self.topics_list_view.item(row)
diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py
index f1901b203..1dc3d9182 100644
--- a/openlp/plugins/songs/forms/editversedialog.py
+++ b/openlp/plugins/songs/forms/editversedialog.py
@@ -37,6 +37,7 @@ from openlp.plugins.songs.lib import VerseType
class Ui_EditVerseDialog(object):
def setupUi(self, edit_verse_dialog):
edit_verse_dialog.setObjectName('edit_verse_dialog')
+ edit_verse_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_verse_dialog.resize(400, 400)
edit_verse_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_verse_dialog)
diff --git a/openlp/plugins/songs/forms/editverseform.py b/openlp/plugins/songs/forms/editverseform.py
index 79a69a015..bc2532b0d 100644
--- a/openlp/plugins/songs/forms/editverseform.py
+++ b/openlp/plugins/songs/forms/editverseform.py
@@ -122,8 +122,6 @@ class EditVerseForm(QtGui.QDialog, Ui_EditVerseDialog):
text = text[:position + 4]
match = VERSE_REGEX.match(text)
if match:
- # TODO: Not used, remove?
- # verse_tag = match.group(1)
try:
verse_num = int(match.group(2)) + 1
except ValueError:
diff --git a/openlp/plugins/songs/forms/mediafilesdialog.py b/openlp/plugins/songs/forms/mediafilesdialog.py
index 3a1db39ed..495e08883 100644
--- a/openlp/plugins/songs/forms/mediafilesdialog.py
+++ b/openlp/plugins/songs/forms/mediafilesdialog.py
@@ -42,10 +42,10 @@ class Ui_MediaFilesDialog(object):
Set up the user interface.
"""
media_files_dialog.setObjectName('media_files_dialog')
+ media_files_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
media_files_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
media_files_dialog.resize(400, 300)
media_files_dialog.setModal(True)
- media_files_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
self.files_vertical_layout = QtGui.QVBoxLayout(media_files_dialog)
self.files_vertical_layout.setSpacing(8)
self.files_vertical_layout.setMargin(8)
diff --git a/openlp/plugins/songs/forms/songbookdialog.py b/openlp/plugins/songs/forms/songbookdialog.py
index 8cacef1a2..1ed79a4eb 100644
--- a/openlp/plugins/songs/forms/songbookdialog.py
+++ b/openlp/plugins/songs/forms/songbookdialog.py
@@ -29,7 +29,7 @@
from PyQt4 import QtGui
-from openlp.core.lib import translate
+from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@@ -42,6 +42,7 @@ class Ui_SongBookDialog(object):
Set up the user interface.
"""
song_book_dialog.setObjectName('song_book_dialog')
+ song_book_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_book_dialog.resize(300, 10)
self.dialog_layout = QtGui.QVBoxLayout(song_book_dialog)
self.dialog_layout.setObjectName('dialog_layout')
diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py
index 27f0d9343..2a05f06cd 100644
--- a/openlp/plugins/songs/forms/songimportform.py
+++ b/openlp/plugins/songs/forms/songimportform.py
@@ -231,11 +231,11 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
"""
Opens a QFileDialog and writes the filenames to the given listbox.
- :param title: The title of the dialog (unicode).
+ :param title: The title of the dialog (str).
:param listbox: A listbox (QListWidget).
- :param filters: The file extension filters. It should contain the file descriptions
- as well as the file extensions. For example::
- u'SongBeamer Files (*.sng)'
+ :param filters: The file extension filters. It should contain the file descriptions as well as the file
+ extensions. For example::
+ 'SongBeamer Files (*.sng)'
"""
if filters:
filters += ';;'
@@ -304,7 +304,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
"""
self.source_page.emit(QtCore.SIGNAL('completeChanged()'))
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default form values for the song import wizard.
"""
diff --git a/openlp/plugins/songs/forms/songmaintenancedialog.py b/openlp/plugins/songs/forms/songmaintenancedialog.py
index 84e3535d3..893ae9c1c 100644
--- a/openlp/plugins/songs/forms/songmaintenancedialog.py
+++ b/openlp/plugins/songs/forms/songmaintenancedialog.py
@@ -44,6 +44,7 @@ class Ui_SongMaintenanceDialog(object):
Set up the user interface for the song maintenance dialog
"""
song_maintenance_dialog.setObjectName('song_maintenance_dialog')
+ song_maintenance_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_maintenance_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
song_maintenance_dialog.resize(10, 350)
self.dialog_layout = QtGui.QGridLayout(song_maintenance_dialog)
diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py
index d3ff5ab52..f9f658c5b 100755
--- a/openlp/plugins/songs/forms/songselectform.py
+++ b/openlp/plugins/songs/forms/songselectform.py
@@ -319,8 +319,6 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
def on_search_finished(self):
"""
Slot which is called when the search is completed.
-
- :param songs:
"""
self.application.process_events()
self.search_progress_bar.setVisible(False)
diff --git a/openlp/plugins/songs/forms/topicsdialog.py b/openlp/plugins/songs/forms/topicsdialog.py
index eb6229bf6..ffa7da333 100644
--- a/openlp/plugins/songs/forms/topicsdialog.py
+++ b/openlp/plugins/songs/forms/topicsdialog.py
@@ -29,7 +29,7 @@
from PyQt4 import QtGui
-from openlp.core.lib import translate
+from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@@ -42,6 +42,7 @@ class Ui_TopicsDialog(object):
Set up the user interface for the topics dialog.
"""
topics_dialog.setObjectName('topics_dialog')
+ topics_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
topics_dialog.resize(300, 10)
self.dialog_layout = QtGui.QVBoxLayout(topics_dialog)
self.dialog_layout.setObjectName('dialog_layout')
diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py
index dc198d4b7..ec14175ac 100644
--- a/openlp/plugins/songs/lib/__init__.py
+++ b/openlp/plugins/songs/lib/__init__.py
@@ -206,14 +206,14 @@ class VerseType(object):
Return the VerseType for a given tag
:param verse_tag: The string to return a VerseType for
- :param default: Default return value if no matching tag is found
+ :param default: Default return value if no matching tag is found (a valid VerseType or None)
:return: A VerseType of the tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
if verse_tag == tag:
return num
- if len(VerseType.names) > default:
+ if default in range(0, len(VerseType.names)) or default is None:
return default
else:
return VerseType.Other
@@ -231,7 +231,7 @@ class VerseType(object):
for num, tag in enumerate(VerseType.translated_tags):
if verse_tag == tag:
return num
- if len(VerseType.names) > default:
+ if default in range(0, len(VerseType.names)) or default is None:
return default
else:
return VerseType.Other
@@ -390,7 +390,7 @@ def clean_song(manager, song):
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one.
- if not song.authors:
+ if not song.authors and not song.authors_songs: # Need to check both relations
name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None:
@@ -434,7 +434,7 @@ def strip_rtf(text, default_encoding=None):
# Current font is the font tag we last met.
font = ''
# Character encoding is defined inside fonttable.
- # font_table could contain eg u'0': u'cp1252'
+ # font_table could contain eg '0': u'cp1252'
font_table = {'': ''}
# Stack of things to keep track of when entering/leaving groups.
stack = []
diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py
index c3965e2ed..91649c951 100644
--- a/openlp/plugins/songs/lib/db.py
+++ b/openlp/plugins/songs/lib/db.py
@@ -35,19 +35,52 @@ import re
from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor
-from sqlalchemy.sql.expression import func
+from sqlalchemy.sql.expression import func, text
from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key
+from openlp.core.lib import translate
class Author(BaseModel):
"""
Author model
"""
+ def get_display_name(self, author_type=None):
+ if author_type:
+ return "%s (%s)" % (self.display_name, AuthorType.Types[author_type])
+ return self.display_name
+
+
+class AuthorSong(BaseModel):
+ """
+ Relationship between Authors and Songs (many to many).
+ Need to define this relationship table explicit to get access to the
+ Association Object (author_type).
+ http://docs.sqlalchemy.org/en/latest/orm/relationships.html#association-object
+ """
pass
+class AuthorType(object):
+ """
+ Enumeration for Author types.
+ They are defined by OpenLyrics: http://openlyrics.info/dataformat.html#authors
+
+ The 'words+music' type is not an official type, but is provided for convenience.
+ """
+ Words = 'words'
+ Music = 'music'
+ WordsAndMusic = 'words+music'
+ Translation = 'translation'
+ Types = {
+ Words: translate('OpenLP.Ui', 'Words'),
+ Music: translate('OpenLP.Ui', 'Music'),
+ WordsAndMusic: translate('OpenLP.Ui', 'Words and Music'),
+ Translation: translate('OpenLP.Ui', 'Translation')
+ }
+
+
class Book(BaseModel):
"""
Book model
@@ -67,6 +100,7 @@ class Song(BaseModel):
"""
Song model
"""
+
def __init__(self):
self.sort_key = []
@@ -120,6 +154,7 @@ def init_schema(url):
* author_id
* song_id
+ * author_type
**media_files Table**
* id
@@ -230,7 +265,8 @@ def init_schema(url):
authors_songs_table = Table(
'authors_songs', metadata,
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
- Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True)
+ Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
+ Column('author_type', types.String(), primary_key=True, nullable=False, server_default=text('""'))
)
# Definition of the "songs_topics" table
@@ -241,10 +277,15 @@ def init_schema(url):
)
mapper(Author, authors_table)
+ mapper(AuthorSong, authors_songs_table, properties={
+ 'author': relation(Author)
+ })
mapper(Book, song_books_table)
mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={
- 'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False),
+ # Use the authors_songs relation when you need access to the 'author_type' attribute.
+ 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"),
+ 'authors': relation(Author, secondary=authors_songs_table),
'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table)
diff --git a/openlp/plugins/songs/lib/easyslidesimport.py b/openlp/plugins/songs/lib/easyslidesimport.py
index e28e7bf97..ca9a9b755 100644
--- a/openlp/plugins/songs/lib/easyslidesimport.py
+++ b/openlp/plugins/songs/lib/easyslidesimport.py
@@ -292,7 +292,7 @@ class EasySlidesImport(SongImport):
return True
def _extract_region(self, line):
- # this was true already: line[0:7] == u'[region':
+ # this was true already: line[0:7] == '[region':
"""
Extract the region from text
diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py
index ca201279a..b08193672 100644
--- a/openlp/plugins/songs/lib/ewimport.py
+++ b/openlp/plugins/songs/lib/ewimport.py
@@ -34,13 +34,13 @@ EasyWorship song databases into the current installation database.
import os
import struct
import re
+import zlib
from openlp.core.lib import translate
from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib import retrieve_windows_encoding, strip_rtf
from .songimport import SongImport
-RTF_STRIPPING_REGEX = re.compile(r'\{\\tx[^}]*\}')
# regex: at least two newlines, can have spaces between them
SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*')
NUMBER_REGEX = re.compile(r'[0-9]+')
@@ -74,12 +74,130 @@ class EasyWorshipSongImport(SongImport):
"""
def __init__(self, manager, **kwargs):
super(EasyWorshipSongImport, self).__init__(manager, **kwargs)
+ self.entry_error_log = ''
def do_import(self):
"""
- Import the songs
+ Determines the type of file to import and calls the appropiate method
+ """
+ if self.import_source.lower().endswith('ews'):
+ self.import_ews()
+ else:
+ self.import_db()
- :return:
+ def import_ews(self):
+ """
+ Import the songs from service file
+ The full spec of the ews files can be found here:
+ https://github.com/meinders/lithium-ews/blob/master/docs/ews%20file%20format.md
+ or here: http://wiki.openlp.org/Development:EasyWorship_EWS_Format
+ """
+ # Open ews file if it exists
+ if not os.path.isfile(self.import_source):
+ log.debug('Given ews file does not exists.')
+ return
+ # Make sure there is room for at least a header and one entry
+ if os.path.getsize(self.import_source) < 892:
+ log.debug('Given ews file is to small to contain valid data.')
+ return
+ # 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
+ self.ews_file = open(self.import_source, 'rb')
+ # EWS header, version '1.6'/' 3'/' 5':
+ # Offset Field Data type Length Details
+ # --------------------------------------------------------------------------------------------------
+ # 0 Filetype string 38 Specifies the file type and version.
+ # "EasyWorship Schedule File Version 1.6" or
+ # "EasyWorship Schedule File Version 3" or
+ # "EasyWorship Schedule File Version 5"
+ # 40/48/56 Entry count int32le 4 Number of items in the schedule
+ # 44/52/60 Entry length int16le 2 Length of schedule entries: 0x0718 = 1816
+ # Get file version
+ type, = struct.unpack('<38s', self.ews_file.read(38))
+ version = type.decode()[-3:]
+ # Set fileposition based on filetype/version
+ file_pos = 0
+ if version == ' 5':
+ file_pos = 56
+ elif version == ' 3':
+ file_pos = 48
+ elif version == '1.6':
+ file_pos = 40
+ else:
+ log.debug('Given ews file is of unknown version.')
+ return
+ entry_count = self.get_i32(file_pos)
+ entry_length = self.get_i16(file_pos+4)
+ file_pos += 6
+ self.import_wizard.progress_bar.setMaximum(entry_count)
+ # Loop over songs
+ for i in range(entry_count):
+ # Load EWS entry metadata:
+ # Offset Field Data type Length Details
+ # ------------------------------------------------------------------------------------------------
+ # 0 Title cstring 50
+ # 307 Author cstring 50
+ # 358 Copyright cstring 100
+ # 459 Administrator cstring 50
+ # 800 Content pointer int32le 4 Position of the content for this entry.
+ # 820 Content type int32le 4 0x01 = Song, 0x02 = Scripture, 0x03 = Presentation,
+ # 0x04 = Video, 0x05 = Live video, 0x07 = Image,
+ # 0x08 = Audio, 0x09 = Web
+ # 1410 Song number cstring 10
+ self.set_defaults()
+ self.title = self.get_string(file_pos + 0, 50)
+ authors = self.get_string(file_pos + 307, 50)
+ copyright = self.get_string(file_pos + 358, 100)
+ admin = self.get_string(file_pos + 459, 50)
+ cont_ptr = self.get_i32(file_pos + 800)
+ cont_type = self.get_i32(file_pos + 820)
+ self.ccli_number = self.get_string(file_pos + 1410, 10)
+ # Only handle content type 1 (songs)
+ if cont_type != 1:
+ file_pos += entry_length
+ continue
+ # Load song content
+ # Offset Field Data type Length Details
+ # ------------------------------------------------------------------------------------------------
+ # 0 Length int32le 4 Length (L) of content, including the compressed content
+ # and the following fields (14 bytes total).
+ # 4 Content string L-14 Content compressed with deflate.
+ # Checksum int32be 4 Alder-32 checksum.
+ # (unknown) 4 0x51 0x4b 0x03 0x04
+ # Content length int32le 4 Length of content after decompression
+ content_length = self.get_i32(cont_ptr)
+ deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
+ deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
+ inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
+ if copyright:
+ self.copyright = copyright
+ if admin:
+ if copyright:
+ self.copyright += ', '
+ self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
+ 'Administered by %s') % admin
+ # Set the SongImport object members.
+ self.set_song_import_object(authors, inflated_content)
+ if self.stop_import_flag:
+ break
+ if self.entry_error_log:
+ self.log_error(self.import_source,
+ translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
+ % (self.title, self.entry_error_log))
+ self.entry_error_log = ''
+ elif not self.finish():
+ self.log_error(self.import_source)
+ # Set file_pos for next entry
+ file_pos += entry_length
+ self.ews_file.close()
+
+ def import_db(self):
+ """
+ Import the songs from the database
"""
# Open the DB and MB files if they exist
import_source_mb = self.import_source.replace('.DB', '.MB')
@@ -169,86 +287,114 @@ class EasyWorshipSongImport(SongImport):
raw_record = db_file.read(record_size)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
- self.title = self.get_field(fi_title).decode()
+ self.title = self.get_field(fi_title).decode('unicode-escape')
# Get remaining fields.
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
- # Set the SongImport object members.
if copy:
- self.copyright = copy.decode()
+ self.copyright = copy.decode('unicode-escape')
if admin:
if copy:
self.copyright += ', '
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
- 'Administered by %s') % admin.decode()
+ 'Administered by %s') % admin.decode('unicode-escape')
if ccli:
- self.ccli_number = ccli.decode()
+ self.ccli_number = ccli.decode('unicode-escape')
if authors:
- # Split up the authors
- author_list = authors.split(b'/')
- if len(author_list) < 2:
- author_list = authors.split(b';')
- if len(author_list) < 2:
- author_list = authors.split(b',')
- for author_name in author_list:
- self.add_author(author_name.decode().strip())
- if words:
- # Format the lyrics
- result = strip_rtf(words.decode(), self.encoding)
- if result is None:
- return
- words, self.encoding = result
- verse_type = VerseType.tags[VerseType.Verse]
- for verse in SLIDE_BREAK_REGEX.split(words):
- verse = verse.strip()
- if not verse:
- continue
- verse_split = verse.split('\n', 1)
- first_line_is_tag = False
- # EW tags: verse, chorus, pre-chorus, bridge, tag,
- # intro, ending, slide
- for tag in VerseType.tags + ['tag', 'slide']:
- tag = tag.lower()
- ew_tag = verse_split[0].strip().lower()
- if ew_tag.startswith(tag):
- verse_type = tag[0]
- if tag == 'tag' or tag == 'slide':
- verse_type = VerseType.tags[VerseType.Other]
- first_line_is_tag = True
- number_found = False
- # check if tag is followed by number and/or note
- if len(ew_tag) > len(tag):
- match = NUMBER_REGEX.search(ew_tag)
- if match:
- number = match.group()
- verse_type += number
- number_found = True
- match = NOTE_REGEX.search(ew_tag)
- if match:
- self.comments += ew_tag + '\n'
- if not number_found:
- verse_type += '1'
- break
- self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
- if len(self.comments) > 5:
- self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
- '\n[above are Song Tags with notes imported from EasyWorship]'))
+ authors = authors.decode('unicode-escape')
+ else:
+ authors = ''
+ # Set the SongImport object members.
+ self.set_song_import_object(authors, words)
if self.stop_import_flag:
break
- if not self.finish():
+ if self.entry_error_log:
+ self.log_error(self.import_source,
+ translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
+ % (self.title, self.entry_error_log))
+ self.entry_error_log = ''
+ elif not self.finish():
self.log_error(self.import_source)
db_file.close()
self.memo_file.close()
+ def set_song_import_object(self, authors, words):
+ """
+ Set the SongImport object members.
+
+ :param authors: String with authons
+ :param words: Bytes with rtf-encoding
+ """
+ if authors:
+ # Split up the authors
+ author_list = authors.split('/')
+ if len(author_list) < 2:
+ author_list = authors.split(';')
+ if len(author_list) < 2:
+ author_list = authors.split(',')
+ for author_name in author_list:
+ self.add_author(author_name.strip())
+ if words:
+ # Format the lyrics
+ result = None
+ decoded_words = None
+ try:
+ decoded_words = words.decode()
+ except UnicodeDecodeError:
+ # The unicode chars in the rtf was not escaped in the expected manor
+ self.entry_error_log = translate('SongsPlugin.EasyWorshipSongImport',
+ 'Unexpected data formatting.')
+ return
+ result = strip_rtf(decoded_words, self.encoding)
+ if result is None:
+ self.entry_error_log = translate('SongsPlugin.EasyWorshipSongImport',
+ 'No song text found.')
+ return
+ words, self.encoding = result
+ verse_type = VerseType.tags[VerseType.Verse]
+ for verse in SLIDE_BREAK_REGEX.split(words):
+ verse = verse.strip()
+ if not verse:
+ continue
+ verse_split = verse.split('\n', 1)
+ first_line_is_tag = False
+ # EW tags: verse, chorus, pre-chorus, bridge, tag,
+ # intro, ending, slide
+ for tag in VerseType.tags + ['tag', 'slide']:
+ tag = tag.lower()
+ ew_tag = verse_split[0].strip().lower()
+ if ew_tag.startswith(tag):
+ verse_type = tag[0]
+ if tag == 'tag' or tag == 'slide':
+ verse_type = VerseType.tags[VerseType.Other]
+ first_line_is_tag = True
+ number_found = False
+ # check if tag is followed by number and/or note
+ if len(ew_tag) > len(tag):
+ match = NUMBER_REGEX.search(ew_tag)
+ if match:
+ number = match.group()
+ verse_type += number
+ number_found = True
+ match = NOTE_REGEX.search(ew_tag)
+ if match:
+ self.comments += ew_tag + '\n'
+ if not number_found:
+ verse_type += '1'
+ break
+ self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
+ if len(self.comments) > 5:
+ self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
+ '\n[above are Song Tags with notes imported from EasyWorship]'))
+
def find_field(self, field_name):
"""
Find a field in the descriptions
:param field_name: field to find
- :return:
"""
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
@@ -285,7 +431,7 @@ class EasyWorshipSongImport(SongImport):
Extract the field
:param field_desc_index: Field index value
- :return:
+ :return: The field value
"""
field = self.fields[field_desc_index]
field_desc = self.field_descriptions[field_desc_index]
@@ -323,3 +469,52 @@ class EasyWorshipSongImport(SongImport):
return self.memo_file.read(blob_size)
else:
return 0
+
+ def get_bytes(self, pos, length):
+ """
+ Get bytes from ews_file
+
+ :param pos: Position to read from
+ :param length: Bytes to read
+ :return: Bytes read
+ """
+ self.ews_file.seek(pos)
+ return self.ews_file.read(length)
+
+ def get_string(self, pos, length):
+ """
+ Get string from ews_file
+
+ :param pos: Position to read from
+ :param length: Characters to read
+ :return: String read
+ """
+ bytes = self.get_bytes(pos, length)
+ mask = '<' + str(length) + 's'
+ byte_str, = struct.unpack(mask, bytes)
+ return byte_str.decode('unicode-escape').replace('\0', '').strip()
+
+ def get_i16(self, pos):
+ """
+ Get short int from ews_file
+
+ :param pos: Position to read from
+ :return: Short integer read
+ """
+
+ bytes = self.get_bytes(pos, 2)
+ mask = '= 2 and value[-1] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
verse_type = "%s%s" % (verse_type, value[-1])
- elif name == 'Hotkey':
- # Hotkey always appears after MARKER_NAME, so it
+ elif name == 'HOTKEY':
+ # HOTKEY always appears after MARKER_NAME, so it
# effectively overrides MARKER_NAME, if present.
if len(value) and value in list(HOTKEY_TO_VERSE_TYPE.keys()):
verse_type = HOTKEY_TO_VERSE_TYPE[value]
- if name == 'rtf':
+ if name == 'RTF':
value = self.unescape(value)
result = strip_rtf(value, self.encoding)
if result is None:
diff --git a/openlp/plugins/songs/lib/ui.py b/openlp/plugins/songs/lib/ui.py
index 14f4777c9..151b11b4b 100644
--- a/openlp/plugins/songs/lib/ui.py
+++ b/openlp/plugins/songs/lib/ui.py
@@ -40,7 +40,7 @@ class SongStrings(object):
# These strings should need a good reason to be retranslated elsewhere.
Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
- AuthorUnknown = 'Author Unknown' # Used to populate the database.
+ AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural')
diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py
index adb7d8af5..580ae767d 100644
--- a/openlp/plugins/songs/lib/upgrade.py
+++ b/openlp/plugins/songs/lib/upgrade.py
@@ -32,14 +32,14 @@ backend for the Songs plugin
"""
import logging
-from sqlalchemy import Column, types
+from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import func, false, null, text
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
-__version__ = 3
+__version__ = 4
def upgrade_1(session, metadata):
@@ -97,3 +97,25 @@ def upgrade_3(session, metadata):
op.add_column('songs', Column('temporary', types.Boolean(), server_default=false()))
except OperationalError:
log.info('Upgrade 3 has already been run')
+
+
+def upgrade_4(session, metadata):
+ """
+ Version 4 upgrade.
+
+ This upgrade adds a column for author type to the authors_songs table
+ """
+ try:
+ # Since SQLite doesn't support changing the primary key of a table, we need to recreate the table
+ # and copy the old values
+ op = get_upgrade_op(session)
+ op.create_table('authors_songs_tmp',
+ Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
+ Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
+ Column('author_type', types.String(), primary_key=True,
+ nullable=False, server_default=text('""')))
+ op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs')
+ op.drop_table('authors_songs')
+ op.rename_table('authors_songs_tmp', 'authors_songs')
+ except OperationalError:
+ log.info('Upgrade 4 has already been run')
diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py
index d516b5e02..87e5da21e 100644
--- a/openlp/plugins/songs/lib/xml.py
+++ b/openlp/plugins/songs/lib/xml.py
@@ -71,7 +71,7 @@ from lxml import etree, objectify
from openlp.core.common import translate
from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
+from openlp.plugins.songs.lib.db import Author, AuthorSong, AuthorType, Book, Song, Topic
from openlp.core.utils import get_application_version
log = logging.getLogger(__name__)
@@ -166,7 +166,7 @@ class OpenLyrics(object):
supported by the :class:`OpenLyrics` class:
````
- OpenLP does not support the attribute *type* and *lang*.
+ OpenLP does not support the attribute *lang*.
````
This property is not supported.
@@ -269,10 +269,18 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower())
if song.ccli_number:
self._add_text_to_element('ccliNo', properties, song.ccli_number)
- if song.authors:
+ if song.authors_songs:
authors = etree.SubElement(properties, 'authors')
- for author in song.authors:
- self._add_text_to_element('author', authors, author.display_name)
+ for author_song in song.authors_songs:
+ element = self._add_text_to_element('author', authors, author_song.author.display_name)
+ if author_song.author_type:
+ # Handle the special case 'words+music': Need to create two separate authors for that
+ if author_song.author_type == AuthorType.WordsAndMusic:
+ element.set('type', AuthorType.Words)
+ element = self._add_text_to_element('author', authors, author_song.author.display_name)
+ element.set('type', AuthorType.Music)
+ else:
+ element.set('type', author_song.author_type)
book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id)
if book is not None:
book = book.name
@@ -302,9 +310,9 @@ class OpenLyrics(object):
verse_tag = verse[0]['type'][0].lower()
verse_number = verse[0]['label']
verse_def = verse_tag + verse_number
- verse_tags.append(verse_def)
# Create the letter from the number of duplicates
- verse[0]['suffix'] = chr(96 + verse_tags.count(verse_def))
+ verse[0][u'suffix'] = chr(97 + (verse_tags.count(verse_def) % 26))
+ verse_tags.append(verse_def)
# If the verse tag is a duplicate use the suffix letter
for verse in verse_list:
verse_tag = verse[0]['type'][0].lower()
@@ -336,7 +344,7 @@ class OpenLyrics(object):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of two unicode strings::
- (u'{st}{r}', u'{/r}{/st}')
+ ('{st}{r}', '{/r}{/st}')
The first unicode string are the start tags (for the next slide). The second unicode string are the end tags.
@@ -501,16 +509,20 @@ class OpenLyrics(object):
if hasattr(properties, 'authors'):
for author in properties.authors.author:
display_name = self._text(author)
+ author_type = author.get('type', '')
if display_name:
- authors.append(display_name)
- for display_name in authors:
+ authors.append((display_name, author_type))
+ for (display_name, author_type) in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None:
# We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1]))
- song.authors.append(author)
+ author_song = AuthorSong()
+ author_song.author = author
+ author_song.author_type = author_type
+ song.authors_songs.append(author_song)
def _process_cclinumber(self, properties, song):
"""
diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py
index b1ddaf412..79fc282a6 100644
--- a/openlp/plugins/songs/songsplugin.py
+++ b/openlp/plugins/songs/songsplugin.py
@@ -63,6 +63,7 @@ __default_settings__ = {
'songs/search as type': False,
'songs/add song from service': True,
'songs/display songbar': True,
+ 'songs/display songbook': False,
'songs/last directory import': '',
'songs/last directory export': '',
'songs/songselect username': '',
diff --git a/openlp/plugins/songusage/forms/songusagedeletedialog.py b/openlp/plugins/songusage/forms/songusagedeletedialog.py
index 01597a790..190292678 100644
--- a/openlp/plugins/songusage/forms/songusagedeletedialog.py
+++ b/openlp/plugins/songusage/forms/songusagedeletedialog.py
@@ -30,6 +30,7 @@
from PyQt4 import QtCore, QtGui
from openlp.core.common import translate
+from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@@ -44,6 +45,7 @@ class Ui_SongUsageDeleteDialog(object):
:param song_usage_delete_dialog:
"""
song_usage_delete_dialog.setObjectName('song_usage_delete_dialog')
+ song_usage_delete_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_usage_delete_dialog.resize(291, 243)
self.vertical_layout = QtGui.QVBoxLayout(song_usage_delete_dialog)
self.vertical_layout.setSpacing(8)
diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py
index ede5075a0..9431c7894 100644
--- a/openlp/plugins/songusage/forms/songusagedetaildialog.py
+++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py
@@ -45,6 +45,7 @@ class Ui_SongUsageDetailDialog(object):
:param song_usage_detail_dialog:
"""
song_usage_detail_dialog.setObjectName('song_usage_detail_dialog')
+ song_usage_detail_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_usage_detail_dialog.resize(609, 413)
self.vertical_layout = QtGui.QVBoxLayout(song_usage_detail_dialog)
self.vertical_layout.setSpacing(8)
diff --git a/resources/images/network_auth.png b/resources/images/network_auth.png
new file mode 100644
index 000000000..45e7a5c17
Binary files /dev/null and b/resources/images/network_auth.png differ
diff --git a/resources/images/network_server.png b/resources/images/network_server.png
new file mode 100644
index 000000000..25b95f3b0
Binary files /dev/null and b/resources/images/network_server.png differ
diff --git a/resources/images/network_ssl.png b/resources/images/network_ssl.png
new file mode 100644
index 000000000..1169de67a
Binary files /dev/null and b/resources/images/network_ssl.png differ
diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc
index ed54e217f..d61166acc 100644
--- a/resources/images/openlp-2.qrc
+++ b/resources/images/openlp-2.qrc
@@ -150,6 +150,11 @@
messagebox_info.png
messagebox_warning.png
+
+ network_server.png
+ network_ssl.png
+ network_auth.png
+
song_usage_active.png
song_usage_inactive.png
diff --git a/scripts/jenkins_script.py b/scripts/jenkins_script.py
index aaee9a71b..eeafbfe23 100644
--- a/scripts/jenkins_script.py
+++ b/scripts/jenkins_script.py
@@ -148,7 +148,7 @@ class JenkinsTrigger(object):
def get_repo_name():
"""
- This returns the name of branch of the wokring directory. For example it returns *lp:~googol/openlp/render*.
+ This returns the name of branch of the working directory. For example it returns *lp:~googol/openlp/render*.
"""
# Run the bzr command.
bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
@@ -198,7 +198,7 @@ def main():
jenkins_trigger = JenkinsTrigger(token)
try:
jenkins_trigger.trigger_build()
- except HTTPError as e:
+ except HTTPError:
print('Wrong token.')
return
# Open the browser before printing the output.
diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py
index 3120f48fa..ab7663a83 100644
--- a/tests/functional/openlp_core_lib/test_file_dialog.py
+++ b/tests/functional/openlp_core_lib/test_file_dialog.py
@@ -53,8 +53,8 @@ class TestFileDialog(TestCase):
self.mocked_os.rest()
self.mocked_qt_gui.reset()
- # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid
- # file names.
+ # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid file
+ # names.
self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [
'/Valid File', '/url%20encoded%20file%20%231', '/non-existing']
self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [
diff --git a/tests/functional/openlp_core_lib/test_image_manager.py b/tests/functional/openlp_core_lib/test_image_manager.py
index 37b6d6fdd..072978993 100644
--- a/tests/functional/openlp_core_lib/test_image_manager.py
+++ b/tests/functional/openlp_core_lib/test_image_manager.py
@@ -171,4 +171,4 @@ class TestImageManager(TestCase, TestMixin):
self.lock.release()
# The sleep time is adjusted in the test case.
time.sleep(self.sleep_time)
- return ''
\ No newline at end of file
+ return ''
diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py
index b4334a728..2e06e47fe 100644
--- a/tests/functional/openlp_core_lib/test_lib.py
+++ b/tests/functional/openlp_core_lib/test_lib.py
@@ -305,7 +305,7 @@ class TestLib(TestCase):
# WHEN: We convert an image to a byte array
result = image_to_byte(mocked_image)
- # THEN: We should receive a value of u'base64mock'
+ # THEN: We should receive a value of 'base64mock'
MockedQtCore.QByteArray.assert_called_with()
MockedQtCore.QBuffer.assert_called_with(mocked_byte_array)
mocked_buffer.open.assert_called_with('writeonly')
diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py
index 025b1a638..f1b8ee17a 100644
--- a/tests/functional/openlp_core_lib/test_ui.py
+++ b/tests/functional/openlp_core_lib/test_ui.py
@@ -82,6 +82,21 @@ class TestUi(TestCase):
self.assertEqual(1, len(btnbox.buttons()))
self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0]))
+ def test_create_horizontal_adjusting_combo_box(self):
+ """
+ Test creating a horizontal adjusting combo box
+ """
+ # GIVEN: A dialog
+ dialog = QtGui.QDialog()
+
+ # WHEN: We create the combobox
+ combo = create_horizontal_adjusting_combo_box(dialog, 'combo1')
+
+ # THEN: We should get a ComboBox
+ self.assertIsInstance(combo, QtGui.QComboBox)
+ self.assertEqual('combo1', combo.objectName())
+ self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy())
+
def test_create_button(self):
"""
Test creating a button
@@ -114,38 +129,6 @@ class TestUi(TestCase):
self.assertEqual('my_btn', btn.objectName())
self.assertTrue(btn.isEnabled())
- def test_create_valign_selection_widgets(self):
- """
- Test creating a combo box for valign selection
- """
- # GIVEN: A dialog
- dialog = QtGui.QDialog()
-
- # WHEN: We create the widgets
- label, combo = create_valign_selection_widgets(dialog)
-
- # THEN: We should get a label and a combobox.
- self.assertEqual(translate('OpenLP.Ui', '&Vertical Align:'), label.text())
- self.assertIsInstance(combo, QtGui.QComboBox)
- self.assertEqual(combo, label.buddy())
- for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
- self.assertTrue(combo.findText(text) >= 0)
-
- def test_create_horizontal_adjusting_combo_box(self):
- """
- Test creating a horizontal adjusting combo box
- """
- # GIVEN: A dialog
- dialog = QtGui.QDialog()
-
- # WHEN: We create the combobox
- combo = create_horizontal_adjusting_combo_box(dialog, 'combo1')
-
- # THEN: We should get a ComboBox
- self.assertIsInstance(combo, QtGui.QComboBox)
- self.assertEqual('combo1', combo.objectName())
- self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy())
-
def test_create_action(self):
"""
Test creating an action
@@ -170,3 +153,47 @@ class TestUi(TestCase):
self.assertIsInstance(action.icon(), QtGui.QIcon)
self.assertEqual('my tooltip', action.toolTip())
self.assertEqual('my statustip', action.statusTip())
+
+ def test_create_valign_selection_widgets(self):
+ """
+ Test creating a combo box for valign selection
+ """
+ # GIVEN: A dialog
+ dialog = QtGui.QDialog()
+
+ # WHEN: We create the widgets
+ label, combo = create_valign_selection_widgets(dialog)
+
+ # THEN: We should get a label and a combobox.
+ self.assertEqual(translate('OpenLP.Ui', '&Vertical Align:'), label.text())
+ self.assertIsInstance(combo, QtGui.QComboBox)
+ self.assertEqual(combo, label.buddy())
+ for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
+ self.assertTrue(combo.findText(text) >= 0)
+
+ def test_find_and_set_in_combo_box(self):
+ """
+ Test finding a string in a combo box and setting it as the selected item if present
+ """
+ # GIVEN: A ComboBox
+ combo = QtGui.QComboBox()
+ combo.addItems(['One', 'Two', 'Three'])
+ combo.setCurrentIndex(1)
+
+ # WHEN: We call the method with a non-existing value and set_missing=False
+ find_and_set_in_combo_box(combo, 'Four', set_missing=False)
+
+ # THEN: The index should not have changed
+ self.assertEqual(1, combo.currentIndex())
+
+ # WHEN: We call the method with a non-existing value
+ find_and_set_in_combo_box(combo, 'Four')
+
+ # THEN: The index should have been reset
+ self.assertEqual(0, combo.currentIndex())
+
+ # WHEN: We call the method with the default behavior
+ find_and_set_in_combo_box(combo, 'Three')
+
+ # THEN: The index should have changed
+ self.assertEqual(2, combo.currentIndex())
diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py
index 9fc6f5137..2e26c286a 100644
--- a/tests/functional/openlp_core_ui/test_firsttimeform.py
+++ b/tests/functional/openlp_core_ui/test_firsttimeform.py
@@ -31,12 +31,12 @@ Package to test the openlp.core.ui.firsttimeform package.
"""
from unittest import TestCase
-from tests.functional import MagicMock
-
-from tests.helpers.testmixin import TestMixin
from openlp.core.common import Registry
from openlp.core.ui.firsttimeform import FirstTimeForm
+from tests.functional import MagicMock
+from tests.helpers.testmixin import TestMixin
+
class TestFirstTimeForm(TestCase, TestMixin):
diff --git a/tests/functional/openlp_core_ui/test_mainwindow.py b/tests/functional/openlp_core_ui/test_mainwindow.py
index 0b17828b9..b348f8f80 100644
--- a/tests/functional/openlp_core_ui/test_mainwindow.py
+++ b/tests/functional/openlp_core_ui/test_mainwindow.py
@@ -34,6 +34,7 @@ import os
from unittest import TestCase
from openlp.core.ui.mainwindow import MainWindow
+from openlp.core.lib.ui import UiStrings
from openlp.core.common.registry import Registry
from tests.utils.constants import TEST_RESOURCES_PATH
from tests.helpers.testmixin import TestMixin
@@ -95,3 +96,41 @@ class TestMainWindow(TestCase, TestMixin):
# THEN the file should not be opened
assert not mocked_load_path.called, 'load_path should not have been called'
+
+ def main_window_title_test(self):
+ """
+ Test that running a new instance of OpenLP set the window title correctly
+ """
+ # GIVEN a newly opened OpenLP instance
+
+ # WHEN no changes are made to the service
+
+ # THEN the main window's title shoud be the same as the OLPV2x string in the UiStrings class
+ self.assertEqual(self.main_window.windowTitle(), UiStrings().OLPV2x,
+ 'The main window\'s title should be the same as the OLPV2x string in UiStrings class')
+
+ def set_service_modifed_test(self):
+ """
+ Test that when setting the service's title the main window's title is set correctly
+ """
+ # GIVEN a newly opened OpenLP instance
+
+ # WHEN set_service_modified is called with with the modified flag set true and a file name
+ self.main_window.set_service_modified(True, 'test.osz')
+
+ # THEN the main window's title should be set to the
+ self.assertEqual(self.main_window.windowTitle(), '%s - %s*' % (UiStrings().OLPV2x, 'test.osz'),
+ 'The main window\'s title should be set to " - test.osz*"')
+
+ def set_service_unmodified_test(self):
+ """
+ Test that when setting the service's title the main window's title is set correctly
+ """
+ # GIVEN a newly opened OpenLP instance
+
+ # WHEN set_service_modified is called with with the modified flag set False and a file name
+ self.main_window.set_service_modified(False, 'test.osz')
+
+ # THEN the main window's title should be set to the
+ self.assertEqual(self.main_window.windowTitle(), '%s - %s' % (UiStrings().OLPV2x, 'test.osz'),
+ 'The main window\'s title should be set to " - test.osz"')
diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py
index d59690949..4c6fa7f86 100644
--- a/tests/functional/openlp_core_ui/test_media.py
+++ b/tests/functional/openlp_core_ui/test_media.py
@@ -125,4 +125,4 @@ class TestMedia(TestCase, TestMixin):
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct')
- self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players')
\ No newline at end of file
+ self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players')
diff --git a/tests/functional/openlp_core_ui/test_slidecontroller.py b/tests/functional/openlp_core_ui/test_slidecontroller.py
index 56d87c511..104c83750 100644
--- a/tests/functional/openlp_core_ui/test_slidecontroller.py
+++ b/tests/functional/openlp_core_ui/test_slidecontroller.py
@@ -33,7 +33,7 @@ from unittest import TestCase
from openlp.core.ui import SlideController
-from tests.interfaces import MagicMock, patch
+from tests.interfaces import MagicMock
class TestSlideController(TestCase):
diff --git a/tests/functional/openlp_core_utils/test_actions.py b/tests/functional/openlp_core_utils/test_actions.py
index 2868f8555..4958c4677 100644
--- a/tests/functional/openlp_core_utils/test_actions.py
+++ b/tests/functional/openlp_core_utils/test_actions.py
@@ -35,9 +35,107 @@ from PyQt4 import QtGui, QtCore
from openlp.core.common import Settings
from openlp.core.utils import ActionList
+from openlp.core.utils.actions import CategoryActionList
+from tests.functional import MagicMock
from tests.helpers.testmixin import TestMixin
+class TestCategoryActionList(TestCase):
+ def setUp(self):
+ """
+ Create an instance and a few example actions.
+ """
+ self.action1 = MagicMock()
+ self.action1.text.return_value = 'first'
+ self.action2 = MagicMock()
+ self.action2.text.return_value = 'second'
+ self.list = CategoryActionList()
+
+ def tearDown(self):
+ """
+ Clean up
+ """
+ del self.list
+
+ def contains_test(self):
+ """
+ Test the __contains__() method
+ """
+ # GIVEN: The list.
+ # WHEN: Add an action
+ self.list.append(self.action1)
+
+ # THEN: The actions should (not) be in the list.
+ self.assertTrue(self.action1 in self.list)
+ self.assertFalse(self.action2 in self.list)
+
+ def len_test(self):
+ """
+ Test the __len__ method
+ """
+ # GIVEN: The list.
+ # WHEN: Do nothing.
+ # THEN: Check the length.
+ self.assertEqual(len(self.list), 0, "The length should be 0.")
+
+ # GIVEN: The list.
+ # WHEN: Append an action.
+ self.list.append(self.action1)
+
+ # THEN: Check the length.
+ self.assertEqual(len(self.list), 1, "The length should be 1.")
+
+ def append_test(self):
+ """
+ Test the append() method
+ """
+ # GIVEN: The list.
+ # WHEN: Append an action.
+ self.list.append(self.action1)
+ self.list.append(self.action2)
+
+ # THEN: Check if the actions are in the list and check if they have the correct weights.
+ self.assertTrue(self.action1 in self.list)
+ self.assertTrue(self.action2 in self.list)
+ self.assertEqual(self.list.actions[0], (0, self.action1))
+ self.assertEqual(self.list.actions[1], (1, self.action2))
+
+ def add_test(self):
+ """
+ Test the add() method
+ """
+ # GIVEN: The list and weights.
+ action1_weight = 42
+ action2_weight = 41
+
+ # WHEN: Add actions and their weights.
+ self.list.add(self.action1, action1_weight)
+ self.list.add(self.action2, action2_weight)
+
+ # THEN: Check if they were added and have the specified weights.
+ self.assertTrue(self.action1 in self.list)
+ self.assertTrue(self.action2 in self.list)
+ # Now check if action1 is second and action2 is first (due to their weights).
+ self.assertEqual(self.list.actions[0], (41, self.action2))
+ self.assertEqual(self.list.actions[1], (42, self.action1))
+
+ def remove_test(self):
+ """
+ Test the remove() method
+ """
+ # GIVEN: The list
+ self.list.append(self.action1)
+
+ # WHEN: Delete an item from the list.
+ self.list.remove(self.action1)
+
+ # THEN: Now the element should not be in the list anymore.
+ self.assertFalse(self.action1 in self.list)
+
+ # THEN: Check if an exception is raised when trying to remove a not present action.
+ self.assertRaises(ValueError, self.list.remove, self.action2)
+
+
class TestActionList(TestCase, TestMixin):
"""
Test the ActionList class
diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py
index a97d757ea..075ecb14f 100644
--- a/tests/functional/openlp_core_utils/test_utils.py
+++ b/tests/functional/openlp_core_utils/test_utils.py
@@ -250,7 +250,7 @@ class TestUtils(TestCase):
# THEN: The user agent is a Linux (or ChromeOS) user agent
result = 'Linux' in user_agent or 'CrOS' in user_agent
- self.assertTrue(result, u'The user agent should be a valid Linux user agent')
+ self.assertTrue(result, 'The user agent should be a valid Linux user agent')
def get_user_agent_windows_test(self):
"""
@@ -265,7 +265,7 @@ class TestUtils(TestCase):
user_agent = _get_user_agent()
# THEN: The user agent is a Linux (or ChromeOS) user agent
- self.assertIn('Windows', user_agent, u'The user agent should be a valid Windows user agent')
+ self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent')
def get_user_agent_macos_test(self):
"""
@@ -280,7 +280,7 @@ class TestUtils(TestCase):
user_agent = _get_user_agent()
# THEN: The user agent is a Linux (or ChromeOS) user agent
- self.assertIn('Mac OS X', user_agent, u'The user agent should be a valid OS X user agent')
+ self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent')
def get_user_agent_default_test(self):
"""
@@ -295,7 +295,7 @@ class TestUtils(TestCase):
user_agent = _get_user_agent()
# THEN: The user agent is a Linux (or ChromeOS) user agent
- self.assertIn('NetBSD', user_agent, u'The user agent should be the default user agent')
+ self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent')
def get_web_page_no_url_test(self):
"""
diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py
index 9e13a448b..037169b88 100644
--- a/tests/functional/openlp_plugins/remotes/test_router.py
+++ b/tests/functional/openlp_plugins/remotes/test_router.py
@@ -32,7 +32,7 @@ This module contains tests for the lib submodule of the Remotes plugin.
import os
from unittest import TestCase
-from openlp.core.common import Settings
+from openlp.core.common import Settings, Registry
from openlp.plugins.remotes.lib.httpserver import HttpRouter
from tests.functional import MagicMock, patch, mock_open
from tests.helpers.testmixin import TestMixin
@@ -92,15 +92,14 @@ class TestRouter(TestCase, TestMixin):
Test the router control functionality
"""
# GIVEN: A testing set of Routes
- router = HttpRouter()
mocked_function = MagicMock()
test_route = [
(r'^/stage/api/poll$', {'function': mocked_function, 'secure': False}),
]
- router.routes = test_route
+ self.router.routes = test_route
# WHEN: called with a poll route
- function, args = router.process_http_request('/stage/api/poll', None)
+ function, args = self.router.process_http_request('/stage/api/poll', None)
# THEN: the function should have been called only once
self.assertEqual(mocked_function, function['function'], 'The mocked function should match defined value.')
@@ -126,6 +125,25 @@ class TestRouter(TestCase, TestMixin):
# THEN: all types should match
self.assertEqual(content_type, header[1], 'Mismatch of content type')
+ def main_poll_test(self):
+ """
+ Test the main poll logic
+ """
+ # GIVEN: a defined router with two slides
+ Registry().register('live_controller', MagicMock)
+ router = HttpRouter()
+ router.send_response = MagicMock()
+ router.send_header = MagicMock()
+ router.end_headers = MagicMock()
+ router.live_controller.slide_count = 2
+
+ # WHEN: main poll called
+ results = router.main_poll()
+
+ # THEN: the correct response should be returned
+ self.assertEqual(results.decode('utf-8'), '{"results": {"slide_count": 2}}',
+ 'The resulting json strings should match')
+
def serve_file_without_params_test(self):
"""
Test the serve_file method without params
diff --git a/tests/functional/openlp_plugins/songs/test_ewimport.py b/tests/functional/openlp_plugins/songs/test_ewimport.py
index 182a6b04a..8d9302015 100644
--- a/tests/functional/openlp_plugins/songs/test_ewimport.py
+++ b/tests/functional/openlp_plugins/songs/test_ewimport.py
@@ -67,7 +67,35 @@ SONG_TEST_DATA = [
'Just to learn from His lips, words of comfort,\nIn the beautiful garden of prayer.', 'v2'),
('There\'s a garden where Jesus is waiting,\nAnd He bids you to come meet Him there,\n'
'Just to bow and receive a new blessing,\nIn the beautiful garden of prayer.', 'v3')],
- 'verse_order_list': []}]
+ 'verse_order_list': []},
+ {'title': 'Vi pløjed og vi så\'de',
+ 'authors': ['Matthias Claudius'],
+ 'copyright': 'Public Domain',
+ 'ccli_number': 0,
+ 'verses':
+ [('Vi pløjed og vi så\'de\nvor sæd i sorten jord,\nså bad vi ham os hjælpe,\nsom højt i Himlen bor,\n'
+ 'og han lod snefald hegne\nmod frosten barsk og hård,\nhan lod det tø og regne\nog varme mildt i vår.',
+ 'v1'),
+ ('Alle gode gaver\nde kommer ovenned,\nså tak da Gud, ja, pris dog Gud\nfor al hans kærlighed!', 'c1'),
+ ('Han er jo den, hvis vilje\nopholder alle ting,\nhan klæder markens lilje\nog runder himlens ring,\n'
+ 'ham lyder vind og vove,\nham rører ravnes nød,\nhvi skulle ej hans småbørn\nda og få dagligt brød?', 'v2'),
+ ('Ja, tak, du kære Fader,\nså mild, så rig, så rund,\nfor korn i hæs og lader,\nfor godt i allen stund!\n'
+ 'Vi kan jo intet give,\nsom nogen ting er værd,\nmen tag vort stakkels hjerte,\nså ringe som det er!', 'v3')],
+ 'verse_order_list': []}]
+
+EWS_SONG_TEST_DATA =\
+ {'title': 'Vi pløjed og vi så\'de',
+ 'authors': ['Matthias Claudius'],
+ 'verses':
+ [('Vi pløjed og vi så\'de\nvor sæd i sorten jord,\nså bad vi ham os hjælpe,\nsom højt i Himlen bor,\n'
+ 'og han lod snefald hegne\nmod frosten barsk og hård,\nhan lod det tø og regne\nog varme mildt i vår.',
+ 'v1'),
+ ('Alle gode gaver\nde kommer ovenned,\nså tak da Gud, ja, pris dog Gud\nfor al hans kærlighed!', 'c1'),
+ ('Han er jo den, hvis vilje\nopholder alle ting,\nhan klæder markens lilje\nog runder himlens ring,\n'
+ 'ham lyder vind og vove,\nham rører ravnes nød,\nhvi skulle ej hans småbørn\nda og få dagligt brød?', 'v2'),
+ ('Ja, tak, du kære Fader,\nså mild, så rig, så rund,\nfor korn i hæs og lader,\nfor godt i allen stund!\n'
+ 'Vi kan jo intet give,\nsom nogen ting er værd,\nmen tag vort stakkels hjerte,\nså ringe som det er!',
+ 'v3')]}
class EasyWorshipSongImportLogger(EasyWorshipSongImport):
@@ -125,6 +153,7 @@ class TestEasyWorshipSongImport(TestCase):
"""
Test the functions in the :mod:`ewimport` module.
"""
+
def create_field_desc_entry_test(self):
"""
Test creating an instance of the :class`FieldDescEntry` class.
@@ -357,9 +386,9 @@ class TestEasyWorshipSongImport(TestCase):
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
mocked_retrieve_windows_encoding.assert_call(encoding)
- def file_import_test(self):
+ def db_file_import_test(self):
"""
- Test the actual import of real song files and check that the imported data is correct.
+ Test the actual import of real song database files 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",
@@ -386,10 +415,11 @@ class TestEasyWorshipSongImport(TestCase):
# WHEN: Importing each file
importer.import_source = os.path.join(TEST_PATH, 'Songs.DB')
+ import_result = importer.do_import()
# THEN: do_import should return none, the song data should be as expected, and finish should have been
# called.
- self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
+ self.assertIsNone(import_result, 'do_import should return None when it has completed')
for song_data in SONG_TEST_DATA:
title = song_data['title']
author_calls = song_data['authors']
@@ -411,3 +441,63 @@ class TestEasyWorshipSongImport(TestCase):
self.assertEqual(importer.verse_order_list, verse_order_list,
'verse_order_list for %s should be %s' % (title, verse_order_list))
mocked_finish.assert_called_with()
+
+ def ews_file_import_test(self):
+ """
+ Test the actual import of song from ews 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.
+ with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
+ patch('openlp.plugins.songs.lib.ewimport.retrieve_windows_encoding') \
+ as mocked_retrieve_windows_encoding:
+ 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 = os.path.join(TEST_PATH, 'test1.ews')
+ 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']
+ self.assertIsNone(import_result, 'do_import should return None when it has completed')
+ self.assertIn(title, 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()
+
+ def import_rtf_unescaped_unicode_test(self):
+ """
+ Test import of rtf without the expected escaping of unicode
+ """
+
+ # GIVEN: A mocked out SongImport class, a mocked out "manager" and mocked out "author" method.
+ with patch('openlp.plugins.songs.lib.ewimport.SongImport'):
+ mocked_manager = MagicMock()
+ mocked_add_author = MagicMock()
+ importer = EasyWorshipSongImportLogger(mocked_manager)
+ importer.add_author = mocked_add_author
+ importer.encoding = 'cp1252'
+
+ # WHEN: running set_song_import_object on a verse string without the needed escaping
+ importer.set_song_import_object('Test Author', b'Det som var fr\x86n begynnelsen')
+
+ # THEN: The import should fail
+ self.assertEquals(importer.entry_error_log, 'Unexpected data formatting.', 'Import should fail')
diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py
index b67c1a4be..140126f26 100644
--- a/tests/functional/openlp_plugins/songs/test_lib.py
+++ b/tests/functional/openlp_plugins/songs/test_lib.py
@@ -445,9 +445,9 @@ class TestVerseType(TestCase):
# THEN: The result should be VerseType.Chorus
self.assertEqual(result, VerseType.Chorus, 'The result should be VerseType.Chorus, but was "%s"' % result)
- def from_tag_with_invalid_default_test(self):
+ def from_tag_with_invalid_intdefault_test(self):
"""
- Test that the from_tag() method returns a sane default when passed an invalid tag and an invalid default.
+ Test that the from_tag() method returns a sane default when passed an invalid tag and an invalid int default.
"""
# GIVEN: A mocked out translate() function that just returns what it was given
with patch('openlp.plugins.songs.lib.translate') as mocked_translate:
@@ -458,3 +458,31 @@ class TestVerseType(TestCase):
# THEN: The result should be VerseType.Other
self.assertEqual(result, VerseType.Other, 'The result should be VerseType.Other, but was "%s"' % result)
+
+ def from_tag_with_invalid_default_test(self):
+ """
+ Test that the from_tag() method returns a sane default when passed an invalid tag and an invalid default.
+ """
+ # GIVEN: A mocked out translate() function that just returns what it was given
+ with patch('openlp.plugins.songs.lib.translate') as mocked_translate:
+ mocked_translate.side_effect = lambda x, y: y
+
+ # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back
+ result = VerseType.from_tag('@', 'asdf')
+
+ # THEN: The result should be VerseType.Other
+ self.assertEqual(result, VerseType.Other, 'The result should be VerseType.Other, but was "%s"' % result)
+
+ def from_tag_with_none_default_test(self):
+ """
+ Test that the from_tag() method returns a sane default when passed an invalid tag and None as default.
+ """
+ # GIVEN: A mocked out translate() function that just returns what it was given
+ with patch('openlp.plugins.songs.lib.translate') as mocked_translate:
+ mocked_translate.side_effect = lambda x, y: y
+
+ # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back
+ result = VerseType.from_tag('m', None)
+
+ # THEN: The result should be None
+ self.assertIsNone(result, 'The result should be None, but was "%s"' % result)
diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py
index 2b5f02483..22291c6a6 100644
--- a/tests/functional/openlp_plugins/songs/test_mediaitem.py
+++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py
@@ -1,8 +1,6 @@
"""
This module contains tests for the lib submodule of the Songs plugin.
"""
-import os
-from tempfile import mkstemp
from unittest import TestCase
from PyQt4 import QtCore, QtGui
@@ -10,6 +8,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.common import Registry, Settings
from openlp.core.lib import ServiceItem
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
+from openlp.plugins.songs.lib.db import AuthorType
from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin
@@ -28,6 +27,7 @@ class TestMediaItem(TestCase, TestMixin):
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
self.media_item = SongMediaItem(None, MagicMock())
+ self.media_item.display_songbook = False
self.get_application()
self.build_settings()
QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
@@ -45,10 +45,12 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item
mock_song = MagicMock()
mock_song.title = 'My Song'
+ mock_song.authors_songs = []
mock_author = MagicMock()
mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
@@ -56,7 +58,7 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'Written by: my author', 'My copyright'],
'The array should be returned correctly with a song, one author and copyright')
self.assertEqual(author_list, ['my author'],
'The author list should be returned correctly with one author')
@@ -68,13 +70,25 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item
mock_song = MagicMock()
mock_song.title = 'My Song'
+ mock_song.authors_songs = []
mock_author = MagicMock()
mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Music
+ mock_song.authors_songs.append(mock_author_song)
mock_author = MagicMock()
mock_author.display_name = 'another author'
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Words
+ mock_song.authors_songs.append(mock_author_song)
+ mock_author = MagicMock()
+ mock_author.display_name = 'translator'
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Translation
+ mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
@@ -82,22 +96,19 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author and another author', 'My copyright'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'Words: another author', 'Music: my author',
+ 'Translation: translator', 'My copyright'],
'The array should be returned correctly with a song, two authors and copyright')
- self.assertEqual(author_list, ['my author', 'another author'],
+ self.assertEqual(author_list, ['another author', 'my author', 'translator'],
'The author list should be returned correctly with two authors')
def build_song_footer_base_ccli_test(self):
"""
- Test build songs footer with basic song and two authors
+ Test build songs footer with basic song and a CCLI number
"""
# GIVEN: A Song and a Service Item and a configured CCLI license
mock_song = MagicMock()
mock_song.title = 'My Song'
- mock_author = MagicMock()
- mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
Settings().setValue('core/ccli number', '1234')
@@ -106,7 +117,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 1234'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 1234'],
'The array should be returned correctly with a song, an author, copyright and ccli')
# WHEN: I amend the CCLI value
@@ -114,5 +125,31 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I would get an amended footer string
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 4321'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 4321'],
'The array should be returned correctly with a song, an author, copyright and amended ccli')
+
+ def build_song_footer_base_songbook_test(self):
+ """
+ Test build songs footer with basic song and a songbook
+ """
+ # GIVEN: A Song and a Service Item
+ mock_song = MagicMock()
+ mock_song.title = 'My Song'
+ mock_song.copyright = 'My copyright'
+ mock_song.book = MagicMock()
+ mock_song.book.name = "My songbook"
+ mock_song.song_number = 12
+ service_item = ServiceItem(None)
+
+ # WHEN: I generate the Footer with default settings
+ self.media_item.generate_footer(service_item, mock_song)
+
+ # THEN: The songbook should not be in the footer
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright'])
+
+ # WHEN: I activate the "display songbook" option
+ self.media_item.display_songbook = True
+ self.media_item.generate_footer(service_item, mock_song)
+
+ # THEN: The songbook should be in the footer
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12'])
diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py
index 7292bb2b0..08400fdc5 100644
--- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py
+++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py
@@ -57,6 +57,8 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
+ self.file_import(os.path.join(TEST_PATH, 'a mighty fortress is our god.sbsong'),
+ self.load_external_result_data(os.path.join(TEST_PATH, 'a mighty fortress is our god.json')))
class TestSongShowPlusImport(TestCase):
diff --git a/tests/interfaces/openlp_core_common/__init__.py b/tests/interfaces/openlp_core_common/__init__.py
new file mode 100644
index 000000000..6b241e7fc
--- /dev/null
+++ b/tests/interfaces/openlp_core_common/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# 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; version 2 of the License. #
+# #
+# 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, write to the Free Software Foundation, Inc., 59 #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
+###############################################################################
diff --git a/tests/interfaces/openlp_core_ui/test_splashscreen.py b/tests/interfaces/openlp_core_common/test_historycombobox.py
similarity index 74%
rename from tests/interfaces/openlp_core_ui/test_splashscreen.py
rename to tests/interfaces/openlp_core_common/test_historycombobox.py
index 35c15f9ec..c0131e46c 100644
--- a/tests/interfaces/openlp_core_ui/test_splashscreen.py
+++ b/tests/interfaces/openlp_core_common/test_historycombobox.py
@@ -27,34 +27,39 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
-Test the openlp.core.ui.splashscreen class.
+Module to test the :mod:`~openlp.core.common.historycombobox` module.
"""
+
from unittest import TestCase
-from PyQt4 import QtGui
+from PyQt4 import QtCore, QtGui, QtTest
-from openlp.core.ui import SplashScreen
+from openlp.core.common import Registry
+from openlp.core.common import HistoryComboBox
from tests.helpers.testmixin import TestMixin
+from tests.interfaces import MagicMock, patch
-class TestSplashScreen(TestCase, TestMixin):
+class TestHistoryComboBox(TestCase, TestMixin):
def setUp(self):
+ Registry.create()
self.get_application()
self.main_window = QtGui.QMainWindow()
+ Registry().register('main_window', self.main_window)
+ self.combo = HistoryComboBox(self.main_window)
def tearDown(self):
- """
- Delete all the C++ objects at the end so that we don't have a segfault
- """
- del self.app
- del self.main_window
+ del self.combo
- def setupUi_test(self):
+ def getItems_test(self):
"""
- Test if the setupUi method....
+ Test the getItems() method
"""
- # GIVEN: A splash screen instance.
- splash = SplashScreen()
+ # GIVEN: The combo.
- # THEN: Check if the splash has a setupUi method.
- assert hasattr(splash, 'setupUi'), 'The Splash Screen should have a setupUi() method.'
+ # WHEN: Add two items.
+ self.combo.addItem('test1')
+ self.combo.addItem('test2')
+
+ # THEN: The list of items should contain both strings.
+ self.assertEqual(self.combo.getItems(), ['test1', 'test2'])
diff --git a/tests/interfaces/openlp_core_lib/test_searchedit.py b/tests/interfaces/openlp_core_lib/test_searchedit.py
index 22bf6fae3..f2cf18988 100644
--- a/tests/interfaces/openlp_core_lib/test_searchedit.py
+++ b/tests/interfaces/openlp_core_lib/test_searchedit.py
@@ -30,7 +30,6 @@
Module to test the EditCustomForm.
"""
from unittest import TestCase
-from unittest.mock import MagicMock
from PyQt4 import QtCore, QtGui, QtTest
@@ -127,9 +126,3 @@ class TestSearchEdit(TestCase, TestMixin):
# THEN: The search edit text should be cleared and the button be hidden.
assert not self.search_edit.text(), "The search edit should not have any text."
assert self.search_edit.clear_button.isHidden(), "The clear button should be hidden."
-
- def resize_event_test(self):
- """
- Just check if the resizeEvent() method is re-implemented.
- """
- assert hasattr(self.search_edit, "resizeEvent"), "The search edit should re-implement the resizeEvent method."
diff --git a/tests/interfaces/openlp_core_ui/test_filerenamedialog.py b/tests/interfaces/openlp_core_ui/test_filerenamedialog.py
index 905f167e9..7d14c7d9c 100644
--- a/tests/interfaces/openlp_core_ui/test_filerenamedialog.py
+++ b/tests/interfaces/openlp_core_ui/test_filerenamedialog.py
@@ -89,7 +89,7 @@ class TestStartFileRenameForm(TestCase, TestMixin):
Test that the file_name_edit setFocus has called with True when executed
"""
# GIVEN: A mocked QDialog.exec_() method and mocked file_name_edit.setFocus() method.
- with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec:
+ with patch('PyQt4.QtGui.QDialog.exec_'):
mocked_set_focus = MagicMock()
self.form.file_name_edit.setFocus = mocked_set_focus
diff --git a/tests/interfaces/openlp_core_ui/test_shortcutlistform.py b/tests/interfaces/openlp_core_ui/test_shortcutlistform.py
new file mode 100644
index 000000000..29c365194
--- /dev/null
+++ b/tests/interfaces/openlp_core_ui/test_shortcutlistform.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# 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; version 2 of the License. #
+# #
+# 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, write to the Free Software Foundation, Inc., 59 #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
+###############################################################################
+"""
+Package to test the openlp.core.ui.shortcutform package.
+"""
+from unittest import TestCase
+
+from PyQt4 import QtCore, QtGui, QtTest
+
+from openlp.core.common import Registry
+from openlp.core.ui.shortcutlistform import ShortcutListForm
+from tests.interfaces import patch
+from tests.helpers.testmixin import TestMixin
+
+
+class TestShortcutform(TestCase, TestMixin):
+
+ def setUp(self):
+ """
+ Create the UI
+ """
+ Registry.create()
+ self.get_application()
+ self.main_window = QtGui.QMainWindow()
+ Registry().register('main_window', self.main_window)
+ self.form = ShortcutListForm()
+
+ def tearDown(self):
+ """
+ Delete all the C++ objects at the end so that we don't have a segfault
+ """
+ del self.form
+ del self.main_window
+
+ def adjust_button_test(self):
+ """
+ Test the _adjust_button() method
+ """
+ # GIVEN: A button.
+ button = QtGui.QPushButton()
+ checked = True
+ enabled = True
+ text = "new!"
+
+ # WHEN: Call the method.
+ with patch('PyQt4.QtGui.QPushButton.setChecked') as mocked_check_method:
+ self.form._adjust_button(button, checked, enabled, text)
+
+ # THEN: The button should be changed.
+ self.assertEqual(button.text(), text, "The text should match.")
+ mocked_check_method.assert_called_once_with(True)
+ self.assertEqual(button.isEnabled(), enabled, "The button should be disabled.")
diff --git a/tests/resources/easyworshipsongs/Songs.DB b/tests/resources/easyworshipsongs/Songs.DB
index 8c9679b86..347695057 100644
Binary files a/tests/resources/easyworshipsongs/Songs.DB and b/tests/resources/easyworshipsongs/Songs.DB differ
diff --git a/tests/resources/easyworshipsongs/Songs.MB b/tests/resources/easyworshipsongs/Songs.MB
index b46323005..949d7755c 100644
Binary files a/tests/resources/easyworshipsongs/Songs.MB and b/tests/resources/easyworshipsongs/Songs.MB differ
diff --git a/tests/resources/easyworshipsongs/test1.ews b/tests/resources/easyworshipsongs/test1.ews
new file mode 100644
index 000000000..2cb9676f1
Binary files /dev/null and b/tests/resources/easyworshipsongs/test1.ews differ
diff --git a/tests/resources/songshowplussongs/a mighty fortress is our god.json b/tests/resources/songshowplussongs/a mighty fortress is our god.json
new file mode 100644
index 000000000..2788ad05c
--- /dev/null
+++ b/tests/resources/songshowplussongs/a mighty fortress is our god.json
@@ -0,0 +1,31 @@
+{
+ "authors": [
+ "Martin Luther"
+ ],
+ "ccli_number": 12456,
+ "comments": "",
+ "copyright": "Public Domain",
+ "song_number": 0,
+ "title": "A Mighty Fortress is our God",
+ "topics": [],
+ "verse_order_list": [],
+ "verses": [
+ [
+ "A mighty fortress is our God, a bulwark never failing;\r\nOur helper He, amid the flood of mortal ills prevailing:\r\nFor still our ancient foe doth seek to work us woe;\r\nHis craft and power are great, and, armed with cruel hate,\r\nOn earth is not his equal.\r\n",
+ "v1"
+ ],
+ [
+ "Did we in our own strength confide, our striving would be losing;\r\nWere not the right Man on our side, the Man of Godâs own choosing:\r\nDost ask who that may be? Christ Jesus, it is He;\r\nLord Sabaoth, His Name, from age to age the same,\r\nAnd He must win the battle.\r\n",
+ "v2"
+ ],
+ [
+ "And though this world, with devils filled, should threaten to undo us,\r\nWe will not fear, for God hath willed His truth to triumph through us:\r\nThe Prince of Darkness grim, we tremble not for him;\r\nHis rage we can endure, for lo, his doom is sure,\r\nOne little word shall fell him.\r\n",
+ "v3"
+ ],
+ [
+ "That word above all earthly powers, no thanks to them, abideth;\r\nThe Spirit and the gifts are ours through Him Who with us sideth:\r\nLet goods and kindred go, this mortal life also;\r\nThe body they may kill: Godâs truth abideth still,\r\nHis kingdom is forever.\r\n",
+ "v4"
+ ]
+ ]
+}
+
diff --git a/tests/resources/songshowplussongs/a mighty fortress is our god.sbsong b/tests/resources/songshowplussongs/a mighty fortress is our god.sbsong
new file mode 100644
index 000000000..b66d52b2b
Binary files /dev/null and b/tests/resources/songshowplussongs/a mighty fortress is our god.sbsong differ
diff --git a/tests/utils/test_bzr_tags.py b/tests/utils/test_bzr_tags.py
index 95017e94f..393f4ce25 100644
--- a/tests/utils/test_bzr_tags.py
+++ b/tests/utils/test_bzr_tags.py
@@ -53,6 +53,7 @@ TAGS = [
['2.0.1', '?'],
['2.0.2', '?'],
['2.0.3', '?'],
+ ['2.0.4', '?'],
['2.1.0', '2119']
]