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