diff --git a/.bzrignore b/.bzrignore index f149d97a7..6377150e0 100644 --- a/.bzrignore +++ b/.bzrignore @@ -2,9 +2,12 @@ *.*~ \#*\# *.eric4project +*.eric5project *.ropeproject *.e4* .eric4project +.komodotools +*.komodoproject list openlp.org 2.0.e4* documentation/build/html diff --git a/openlp/__init__.py b/openlp/__init__.py index ac5f063d4..98e847979 100644 --- a/openlp/__init__.py +++ b/openlp/__init__.py @@ -30,7 +30,6 @@ The :mod:`openlp` module contains all the project produced OpenLP functionality """ -import openlp.core -import openlp.plugins +from openlp import core, plugins __all__ = ['core', 'plugins'] diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 8a8e6eef3..22207dec4 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -38,7 +38,7 @@ import traceback from PyQt4 import QtCore -log = logging.getLogger(__name__+'.__init__') +log = logging.getLogger(__name__ + '.__init__') FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)') @@ -76,6 +76,9 @@ def check_directory_exists(directory, do_not_log=False): def get_frozen_path(frozen_option, non_frozen_option): """ Return a path based on the system status. + + :param frozen_option: + :param non_frozen_option: """ if hasattr(sys, 'frozen') and sys.frozen == 1: return frozen_option diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index e6b7f4fe3..9561baff4 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -39,7 +39,7 @@ from PyQt4 import QtCore, QtGui, Qt from openlp.core.common import translate -log = logging.getLogger(__name__+'.__init__') +log = logging.getLogger(__name__ + '.__init__') class ServiceItemContext(object): 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/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 13d4b0bbb..4d7676ad6 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -168,29 +168,29 @@ class MediaManagerItem(QtGui.QWidget, RegistryProperties): Create buttons for the media item toolbar """ toolbar_actions = [] - ## Import Button ## + # Import Button if self.has_import_icon: toolbar_actions.append(['Import', StringContent.Import, ':/general/general_import.png', self.on_import_click]) - ## Load Button ## + # Load Button if self.has_file_icon: toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click]) - ## New Button ## + # New Button if self.has_new_icon: toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click]) - ## Edit Button ## + # Edit Button if self.has_edit_icon: toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click]) - ## Delete Button ## + # Delete Button if self.has_delete_icon: toolbar_actions.append(['Delete', StringContent.Delete, ':/general/general_delete.png', self.on_delete_click]) - ## Preview ## + # Preview toolbar_actions.append(['Preview', StringContent.Preview, ':/general/general_preview.png', self.on_preview_click]) - ## Live Button ## + # Live Button toolbar_actions.append(['Live', StringContent.Live, ':/general/general_live.png', self.on_live_click]) - ## Add to service Button ## + # Add to service Button toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click]) for action in toolbar_actions: if action[0] == StringContent.Preview: diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index e14fe8bb0..1f459524c 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -101,7 +101,7 @@ class Plugin(QtCore.QObject, RegistryProperties): ``add_import_menu_item(import_menu)`` Add an item to the Import menu. - ``add_export_menu_Item(export_menu)`` + ``add_export_menu_item(export_menu)`` Add an item to the Export menu. ``create_settings_tab()`` @@ -226,7 +226,7 @@ class Plugin(QtCore.QObject, RegistryProperties): """ pass - def add_export_menu_Item(self, export_menu): + def add_export_menu_item(self, export_menu): """ Create a menu item and add it to the "Export" menu. @@ -329,22 +329,24 @@ class Plugin(QtCore.QObject, RegistryProperties): def set_plugin_ui_text_strings(self, tooltips): """ Called to define all translatable texts of the plugin + + :param tooltips: """ - ## Load Action ## + # Load Action self.__set_name_text_string(StringContent.Load, UiStrings().Load, tooltips['load']) - ## Import Action ## + # Import Action self.__set_name_text_string(StringContent.Import, UiStrings().Import, tooltips['import']) - ## New Action ## + # New Action self.__set_name_text_string(StringContent.New, UiStrings().Add, tooltips['new']) - ## Edit Action ## + # Edit Action self.__set_name_text_string(StringContent.Edit, UiStrings().Edit, tooltips['edit']) - ## Delete Action ## + # Delete Action self.__set_name_text_string(StringContent.Delete, UiStrings().Delete, tooltips['delete']) - ## Preview Action ## + # Preview Action self.__set_name_text_string(StringContent.Preview, UiStrings().Preview, tooltips['preview']) - ## Send Live Action ## + # Send Live Action self.__set_name_text_string(StringContent.Live, UiStrings().Live, tooltips['live']) - ## Add to Service Action ## + # Add to Service Action self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service']) def __set_name_text_string(self, name, title, tooltip): diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index d24f07eac..474113c98 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -161,7 +161,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): """ for plugin in self.plugins: if plugin.status is not PluginStatus.Disabled: - plugin.add_export_menu_Item(self.main_window.file_export_menu) + plugin.add_export_menu_item(self.main_window.file_export_menu) def hook_tools_menu(self): """ 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/serviceitem.py b/openlp/core/lib/serviceitem.py index 5d7bf4053..17d11ef63 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -387,7 +387,7 @@ class ServiceItem(RegistryProperties): self.will_auto_start = header.get('will_auto_start', False) self.processor = header.get('processor', None) self.has_original_files = True - #TODO Remove me in 2,3 build phase + # TODO: Remove me in 2,3 build phase if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay): self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay) self.processor = self.title diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index 3126d1a56..965adb053 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -173,7 +173,7 @@ def create_button(parent, name, **kwargs): kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.')) else: log.warn('The role "%s" is not defined in create_push_button().', role) - if kwargs.pop('class', '') == 'toolbutton': + if kwargs.pop('btn_class', '') == 'toolbutton': button = QtGui.QToolButton(parent) else: button = QtGui.QPushButton(parent) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index b2c8cd14b..a5ce09bdf 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab): """ Select an image for the default display screen. """ - file_filters = '%s;;%s (*.*) (*)' % (get_images_filter(), UiStrings().AllFiles) + file_filters = '%s;;%s (*.*)' % (get_images_filter(), UiStrings().AllFiles) filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.AdvancedTab', 'Open File'), '', file_filters) if filename: diff --git a/openlp/core/ui/exceptiondialog.py b/openlp/core/ui/exceptiondialog.py index 4d7abe708..212fee4cd 100644 --- a/openlp/core/ui/exceptiondialog.py +++ b/openlp/core/ui/exceptiondialog.py @@ -95,15 +95,15 @@ class Ui_ExceptionDialog(object): Translate the widgets on the fly. """ exception_dialog.setWindowTitle(translate('OpenLP.ExceptionDialog', 'Error Occurred')) - self.description_explanation.setText(translate('OpenLP.ExceptionDialog', - 'Please enter a description of what you were doing to cause this error ' - '\n(Minimum 20 characters)')) - self.message_label.setText(translate('OpenLP.ExceptionDialog', 'Oops! ' - 'OpenLP hit a problem, and couldn\'t recover. The text in the box ' - 'below contains information that might be helpful to the OpenLP ' - 'developers, so please e-mail it to bugs@openlp.org, along with a ' - 'detailed description of what you were doing when the problem ' - 'occurred.')) + self.description_explanation.setText( + translate('OpenLP.ExceptionDialog', 'Please enter a description of what you were doing to cause this error ' + '\n(Minimum 20 characters)')) + self.message_label.setText( + translate('OpenLP.ExceptionDialog', 'Oops! OpenLP hit a problem, and couldn\'t recover. The text in the ' + 'box below contains information that might be helpful to the OpenLP ' + 'developers, so please e-mail it to bugs@openlp.org, along with a ' + 'detailed description of what you were doing when the problem ' + 'occurred.')) self.send_report_button.setText(translate('OpenLP.ExceptionDialog', 'Send E-Mail')) self.save_report_button.setText(translate('OpenLP.ExceptionDialog', 'Save to File')) self.attach_tile_button.setText(translate('OpenLP.ExceptionDialog', 'Attach File')) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index ae3bc9db0..e0228a43b 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -228,7 +228,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties): """ files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), Settings().value(self.settings_section + '/last directory'), - '%s (*.*) (*)' % UiStrings().AllFiles) + '%s (*)' % UiStrings().AllFiles) log.info('New files(s) %s', str(files)) if files: self.file_attachment = str(files) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index aa89da6c0..b9b5f5997 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -67,7 +67,7 @@ class ThemeScreenshotThread(QtCore.QThread): title = config.get('theme_%s' % theme, 'title') filename = config.get('theme_%s' % theme, 'filename') screenshot = config.get('theme_%s' % theme, 'screenshot') - urllib.request.urlretrieve('%s%s' % (self.parent().web, screenshot), + urllib.request.urlretrieve('%s%s' % (self.parent().themes_url, screenshot), os.path.join(gettempdir(), 'openlp', screenshot)) item = QtGui.QListWidgetItem(title, self.parent().themes_list_widget) item.setData(QtCore.Qt.UserRole, filename) @@ -96,6 +96,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): if self.web_access: files = self.web_access.read() self.config.read_string(files.decode()) + self.web = self.config.get('general', 'base url') + self.songs_url = self.web + self.config.get('songs', 'directory') + '/' + self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' + self.themes_url = self.web + self.config.get('themes', 'directory') + '/' self.update_screen_list_combo() self.was_download_cancelled = False self.theme_screenshot_thread = None @@ -110,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. """ @@ -341,7 +345,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): item = self.songs_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.web, filename)) + size = self._get_file_size('%s%s' % (self.songs_url, filename)) self.max_progress += size # Loop through the Bibles list and increase for each selected item iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -350,7 +354,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): item = iterator.value() if item.parent() and item.checkState(0) == QtCore.Qt.Checked: filename = item.data(0, QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.web, filename)) + size = self._get_file_size('%s%s' % (self.bibles_url, filename)) self.max_progress += size iterator += 1 # Loop through the themes list and increase for each selected item @@ -359,7 +363,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): item = self.themes_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('%s%s' % (self.web, filename)) + size = self._get_file_size('%s%s' % (self.themes_url, filename)) self.max_progress += size if self.max_progress: # Add on 2 for plugins status setting plus a "finished" point. @@ -435,7 +439,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading % filename, 0) self.previous_size = 0 destination = os.path.join(songs_destination, str(filename)) - self.url_get_file('%s%s' % (self.web, filename), destination) + self.url_get_file('%s%s' % (self.songs_url, filename), destination) # Download Bibles bibles_iterator = QtGui.QTreeWidgetItemIterator( self.bibles_tree_widget) @@ -445,7 +449,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): bible = item.data(0, QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading % bible, 0) self.previous_size = 0 - self.url_get_file('%s%s' % (self.web, bible), os.path.join(bibles_destination, bible)) + self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible)) bibles_iterator += 1 # Download themes for i in range(self.themes_list_widget.count()): @@ -454,7 +458,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): theme = item.data(QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading % theme, 0) self.previous_size = 0 - self.url_get_file('%s%s' % (self.web, theme), os.path.join(themes_destination, theme)) + self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme)) # Set Default Display if self.display_combo_box.currentIndex() != -1: Settings().setValue('core/monitor', self.display_combo_box.currentIndex()) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 407842c48..1a270f931 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -211,9 +211,9 @@ class Ui_FirstTimeWizard(object): first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard')) self.title_label.setText('%s' % translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')) - self.information_label.setText(translate('OpenLP.FirstTimeWizard', - 'This wizard will help you to configure OpenLP for initial use. ' - 'Click the next button below to start.')) + self.information_label.setText( + translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' + 'Click the next button below to start.')) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index b7153429d..be4247bc1 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -63,7 +63,6 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont self.services = FormattingTagController() self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) self.new_button.clicked.connect(self.on_new_clicked) - #self.save_button.clicked.connect(self.on_saved_clicked) self.delete_button.clicked.connect(self.on_delete_clicked) self.tag_table_widget.currentCellChanged.connect(self.on_current_cell_changed) self.button_box.rejected.connect(self.close) @@ -202,5 +201,4 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont if errors: QtGui.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors, QtGui.QMessageBox.Ok) - #self.tag_table_widget.selectRow(pre_row - 1) self.tag_table_widget.resizeRowsToContents() diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index b9423046e..946299aca 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -168,8 +168,10 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): """ if enabled: self.setAutoFillBackground(False) + self.setStyleSheet("QGraphicsView {background: transparent; border: 0px;}") else: self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False) + self.setStyleSheet("QGraphicsView {}") self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled) self.repaint() @@ -350,7 +352,6 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.hide_display(self.hide_mode) # Only continue if the visibility wasn't changed during method call. elif was_visible == self.isVisible(): - # Single screen active if self.screens.display_count == 1: # Only make visible if setting enabled. diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 7bb445fb3..9c193b079 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -56,29 +56,27 @@ from openlp.core.ui.firsttimeform import FirstTimeForm log = logging.getLogger(__name__) MEDIA_MANAGER_STYLE = """ - QToolBox { +QToolBox { padding-bottom: 2px; - } - QToolBox::tab { +} +QToolBox::tab { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 palette(button), stop: 0.5 palette(button), - stop: 1.0 palette(mid)); - border: 1px groove palette(mid); - border-radius: 5px; - } - QToolBox::tab:selected { + stop: 0 palette(button), stop: 1.0 palette(mid)); + border: 1px solid palette(mid); + border-radius: 3px; +} +QToolBox::tab:selected { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 palette(light), stop: 0.5 palette(midlight), - stop: 1.0 palette(dark)); - border: 1px groove palette(dark); + stop: 0 palette(light), stop: 1.0 palette(button)); + border: 1px solid palette(mid); font-weight: bold; - } +} """ PROGRESSBAR_STYLE = """ - QProgressBar{ - height: 10px; - } +QProgressBar{ + height: 10px; +} """ @@ -369,7 +367,7 @@ class Ui_MainWindow(object): self.settings_menu.setTitle(translate('OpenLP.MainWindow', '&Settings')) self.settings_language_menu.setTitle(translate('OpenLP.MainWindow', '&Language')) self.help_menu.setTitle(translate('OpenLP.MainWindow', '&Help')) - self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Media Manager')) + self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library')) self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager')) self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager')) self.file_new_item.setText(translate('OpenLP.MainWindow', '&New')) @@ -396,12 +394,12 @@ class Ui_MainWindow(object): self.settings_shortcuts_item.setText(translate('OpenLP.MainWindow', 'Configure &Shortcuts...')) self.formatting_tag_item.setText(translate('OpenLP.MainWindow', 'Configure &Formatting Tags...')) self.settings_configure_item.setText(translate('OpenLP.MainWindow', '&Configure OpenLP...')) - self.settings_export_item.setStatusTip(translate('OpenLP.MainWindow', - 'Export OpenLP settings to a specified *.config file')) + self.settings_export_item.setStatusTip( + translate('OpenLP.MainWindow', 'Export OpenLP settings to a specified *.config file')) self.settings_export_item.setText(translate('OpenLP.MainWindow', 'Settings')) - self.settings_import_item.setStatusTip(translate('OpenLP.MainWindow', - 'Import OpenLP settings from a specified *.config file previously ' - 'exported on this or another machine')) + self.settings_import_item.setStatusTip( + translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously ' + 'exported on this or another machine')) self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings')) self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager')) self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager')) @@ -862,7 +860,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): section = 'general' section_key = section + "/" + key # Make sure it's a valid section for us. - if not section in setting_sections: + if section not in setting_sections: continue # We have a good file, import it. for section_key in import_keys: @@ -1336,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) @@ -1366,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/__init__.py b/openlp/core/ui/media/__init__.py index 1276c8662..b2b6ab0b8 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -35,7 +35,7 @@ from openlp.core.common import Settings from PyQt4 import QtCore -log = logging.getLogger(__name__+'.__init__') +log = logging.getLogger(__name__ + '.__init__') class MediaState(object): @@ -90,7 +90,7 @@ def get_media_players(): overridden_player = 'auto' else: overridden_player = '' - saved_players_list = saved_players.replace('[', '').replace(']', '').split(',') + saved_players_list = saved_players.replace('[', '').replace(']', '').split(',') if saved_players else [] return saved_players_list, overridden_player diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index d846af0e4..596b618cb 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -137,7 +137,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): for player in list(self.media_players.values()): if player.is_active: for item in player.audio_extensions_list: - if not item in self.audio_extensions_list: + if item not in self.audio_extensions_list: self.audio_extensions_list.append(item) suffix_list.append(item[2:]) self.video_extensions_list = [] @@ -184,8 +184,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): return False saved_players, overridden_player = get_media_players() invalid_media_players = \ - [mediaPlayer for mediaPlayer in saved_players if not mediaPlayer in self.media_players or - not self.media_players[mediaPlayer].check_available()] + [media_player for media_player in saved_players if media_player not in self.media_players or + not self.media_players[media_player].check_available()] if invalid_media_players: for invalidPlayer in invalid_media_players: saved_players.remove(invalidPlayer) @@ -506,7 +506,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): else: self.media_volume(controller, controller.media_info.volume) if status: - display.frame.evaluateJavaScript('show_blank("desktop");') + if not controller.media_info.is_background: + display.frame.evaluateJavaScript('show_blank("desktop");') self.current_media_players[controller.controller_type].set_visible(display, True) # Flash needs to be played and will not AutoPlay if controller.media_info.is_flash: @@ -517,7 +518,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.mediabar.actions['playbackPause'].setVisible(True) controller.mediabar.actions['playbackStop'].setVisible(True) if controller.is_live: - if controller.hide_menu.defaultAction().isChecked(): + if controller.hide_menu.defaultAction().isChecked() and not controller.media_info.is_background: controller.hide_menu.defaultAction().trigger() # Start Timer for ui updates if not self.timer.isActive(): diff --git a/openlp/core/ui/media/webkitplayer.py b/openlp/core/ui/media/webkitplayer.py index e3a522140..38e691494 100644 --- a/openlp/core/ui/media/webkitplayer.py +++ b/openlp/core/ui/media/webkitplayer.py @@ -174,34 +174,11 @@ FLASH_HTML = """ """ -VIDEO_EXT = [ - '*.3gp', - '*.3gpp', - '*.3g2', - '*.3gpp2', - '*.aac', - '*.flv', - '*.f4a', - '*.f4b', - '*.f4p', - '*.f4v', - '*.mov', - '*.m4a', - '*.m4b', - '*.m4p', - '*.m4v', - '*.mkv', - '*.mp4', - '*.ogv', - '*.webm', - '*.mpg', '*.wmv', '*.mpeg', '*.avi', - '*.swf' -] +VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov', + '*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg', + '*.avi', '*.swf'] -AUDIO_EXT = [ - '*.mp3', - '*.ogg' -] +AUDIO_EXT = ['*.mp3', '*.ogg'] class WebkitPlayer(MediaPlayer): @@ -411,10 +388,9 @@ class WebkitPlayer(MediaPlayer): """ Return some information about this player """ - return(translate('Media.player', 'Webkit is a media player which runs ' - 'inside a web browser. This player allows text over video to be ' - 'rendered.') + - '
' + translate('Media.player', 'Audio') + - '
' + str(AUDIO_EXT) + '
' + - translate('Media.player', 'Video') + '
' + - str(VIDEO_EXT) + '
') + part1 = translate('Media.player', 'Webkit is a media player which runs inside a web browser. This player ' + 'allows text over video to be rendered.') + part2 = translate('Media.player', 'Audio') + part3 = translate('Media.player', 'Video') + return part1 + '
' + part2 + '
' + str(AUDIO_EXT) + '
' + part3 + \ + '
' + str(VIDEO_EXT) + '
' 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/servicemanager.py b/openlp/core/ui/servicemanager.py index 4489360da..70cbd6141 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -235,7 +235,8 @@ class Ui_ServiceManager(object): self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Edit Item'), icon=':/general/general_edit.png', triggers=self.remote_edit) self.rename_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Rename...'), - icon=':/general/general_edit.png', triggers=self.on_service_item_rename) + icon=':/general/general_edit.png', + triggers=self.on_service_item_rename) self.maintain_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Reorder Item'), icon=':/general/general_edit.png', triggers=self.on_service_item_edit_form) @@ -401,7 +402,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage :param suffix_list: New Suffix's to be supported """ for suffix in suffix_list: - if not suffix in self.suffixes: + if suffix not in self.suffixes: self.suffixes.append(suffix) def on_new_service_clicked(self, field=None): @@ -631,7 +632,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage for item in self.service_items: self.main_window.increment_progress_bar() service_item = item['service_item'].get_service_repr(self._save_lite) - #TODO: check for file item on save. + # TODO: check for file item on save. service.append({'serviceitem': service_item}) self.main_window.increment_progress_bar() service_content = json.dumps(service) @@ -756,8 +757,9 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage items = json.load(file_to) else: critical_error_message_box(message=translate('OpenLP.ServiceManager', - 'The service file you are trying to open is in an old format.\n ' - 'Please save it using OpenLP 2.0.2 or greater.')) + 'The service file you are trying to open is in an old ' + 'format.\n Please save it using OpenLP 2.0.2 or ' + 'greater.')) return file_to.close() self.new_file() @@ -1487,9 +1489,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage if new_item: self.add_service_item(new_item, replace=True) - def on_service_item_rename(self): + def on_service_item_rename(self, field=None): """ Opens a dialog to rename the service item. + + :param field: Not used, but PyQt needs this. """ item = self.find_service_item()[0] if not self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEditTitle): diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index 5aba66ef2..a1077e1f4 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -150,5 +150,5 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties): :param function: The function to be called """ - if not function in self.processes: + if function not in self.processes: self.processes.append(function) 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/slidecontroller.py b/openlp/core/ui/slidecontroller.py index ed3ddaeda..bf92e9d76 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -495,14 +495,14 @@ class SlideController(DisplayController, RegistryProperties): self.on_theme_display(False) self.on_hide_display(False) - def service_previous(self): + def service_previous(self, field=None): """ Live event to select the previous service item from the service manager. """ self.keypress_queue.append(ServiceItemAction.Previous) self._process_queue() - def service_next(self): + def service_next(self, field=None): """ Live event to select the next service item from the service manager. """ diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index fbfc1035c..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) @@ -432,7 +432,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): Background Image button pushed. """ images_filter = get_images_filter() - images_filter = '%s;;%s (*.*) (*)' % (images_filter, UiStrings().AllFiles) + images_filter = '%s;;%s (*.*)' % (images_filter, UiStrings().AllFiles) filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.ThemeWizard', 'Select Image'), '', images_filter) if filename: diff --git a/openlp/core/ui/themelayoutdialog.py b/openlp/core/ui/themelayoutdialog.py index bf6c1069a..023d6259c 100644 --- a/openlp/core/ui/themelayoutdialog.py +++ b/openlp/core/ui/themelayoutdialog.py @@ -44,7 +44,6 @@ class Ui_ThemeLayoutDialog(object): Set up the UI """ themeLayoutDialog.setObjectName('themeLayoutDialogDialog') - #themeLayoutDialog.resize(300, 200) 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 32ed30303..1f54f984b 100644 --- a/openlp/core/ui/themestab.py +++ b/openlp/core/ui/themestab.py @@ -114,17 +114,19 @@ class ThemesTab(SettingsTab): self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme')) self.level_group_box.setTitle(translate('OpenLP.ThemesTab', 'Theme Level')) self.song_level_radio_button.setText(translate('OpenLP.ThemesTab', 'S&ong Level')) - self.song_level_label.setText(translate('OpenLP.ThemesTab', 'Use the theme from each song ' - 'in the database. If a song doesn\'t have a theme associated with ' - 'it, then use the service\'s theme. If the service doesn\'t have ' - 'a theme, then use the global theme.')) + self.song_level_label.setText( + translate('OpenLP.ThemesTab', 'Use the theme from each song in the database. If a song doesn\'t have a ' + 'theme associated with it, then use the service\'s theme. If the service ' + 'doesn\'t have a theme, then use the global theme.')) self.service_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Service Level')) - self.service_level_label.setText(translate('OpenLP.ThemesTab', 'Use the theme from the service, ' - 'overriding any of the individual songs\' themes. If the ' - 'service doesn\'t have a theme, then use the global theme.')) + self.service_level_label.setText( + translate('OpenLP.ThemesTab', 'Use the theme from the service, overriding any of the individual ' + 'songs\' themes. If the service doesn\'t have a theme, then use the global ' + 'theme.')) self.global_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Global Level')) - self.global_level_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding ' - 'any themes associated with either the service or the songs.')) + self.global_level_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding any themes ' + 'associated with either the service or the ' + 'songs.')) def load(self): """ diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 05951d14a..5815457b5 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -197,7 +197,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): """ Run the wizard. """ - self.setDefaults() + self.set_defaults() return QtGui.QWizard.exec_(self) def reject(self): diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 746c50d71..a5b5f356a 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -56,7 +56,7 @@ if sys.platform != 'win32' and sys.platform != 'darwin': from openlp.core.common import translate -log = logging.getLogger(__name__+'.__init__') +log = logging.getLogger(__name__ + '.__init__') APPLICATION_VERSION = {} IMAGES_FILTER = None 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/alertsplugin.py b/openlp/plugins/alerts/alertsplugin.py index cf3b7d0ce..76efadf32 100644 --- a/openlp/plugins/alerts/alertsplugin.py +++ b/openlp/plugins/alerts/alertsplugin.py @@ -207,12 +207,12 @@ class AlertsPlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('AlertsPlugin', 'Alert', 'name singular'), 'plural': translate('AlertsPlugin', 'Alerts', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('AlertsPlugin', 'Alerts', 'container title') } diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index 5a51be163..e7f1fdd56 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -88,8 +88,6 @@ class BiblePlugin(Plugin): self.import_bible_item.setVisible(True) action_list = ActionList.get_instance() action_list.add_action(self.import_bible_item, UiStrings().Import) - # Do not add the action to the list yet. - #action_list.add_action(self.export_bible_item, UiStrings().Export) # Set to invisible until we can export bibles self.export_bible_item.setVisible(False) self.tools_upgrade_item.setVisible(bool(self.manager.old_bible_databases)) @@ -104,7 +102,6 @@ class BiblePlugin(Plugin): action_list = ActionList.get_instance() action_list.remove_action(self.import_bible_item, UiStrings().Import) self.import_bible_item.setVisible(False) - #action_list.remove_action(self.export_bible_item, UiStrings().Export) self.export_bible_item.setVisible(False) def app_startup(self): @@ -115,19 +112,27 @@ class BiblePlugin(Plugin): if self.manager.old_bible_databases: if QtGui.QMessageBox.information( self.main_window, translate('OpenLP', 'Information'), - translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your existing Bibles.\n' - 'Should OpenLP upgrade now?'), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \ + translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your ' + 'existing Bibles.\nShould OpenLP upgrade now?'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \ QtGui.QMessageBox.Yes: self.on_tools_upgrade_Item_triggered() def add_import_menu_item(self, import_menu): + """ + + :param import_menu: + """ self.import_bible_item = create_action(import_menu, 'importBibleItem', text=translate('BiblesPlugin', '&Bible'), visible=False, triggers=self.on_bible_import_click) import_menu.addAction(self.import_bible_item) - def add_export_menu_Item(self, export_menu): + def add_export_menu_item(self, export_menu): + """ + + :param export_menu: + """ self.export_bible_item = create_action(export_menu, 'exportBibleItem', text=translate('BiblesPlugin', '&Bible'), visible=False) export_menu.addAction(self.export_bible_item) @@ -190,12 +195,12 @@ class BiblePlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('BiblesPlugin', 'Bible', 'name singular'), 'plural': translate('BiblesPlugin', 'Bibles', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('BiblesPlugin', 'Bibles', 'container title') } 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/lib/db.py b/openlp/plugins/bibles/lib/db.py index 8011c97c1..203d86406 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -32,9 +32,11 @@ import logging import os import re import sqlite3 +import time from PyQt4 import QtCore from sqlalchemy import Column, ForeignKey, Table, or_, types, func +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import class_mapper, mapper, relation from sqlalchemy.orm.exc import UnmappedClassError @@ -235,7 +237,12 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties): text=verse_text ) self.session.add(verse) - self.session.commit() + try: + self.session.commit() + except OperationalError: + # Wait 10ms and try again (lp#1154467) + time.sleep(0.01) + self.session.commit() def create_verse(self, book_id, chapter, verse, text): """ @@ -649,10 +656,10 @@ class BiblesResourcesDB(QtCore.QObject, Manager): 'chapter, verse_count FROM chapters WHERE book_reference_id = ?', (book_ref_id,)) try: return { - 'id': chapters[chapter-1][0], - 'book_reference_id': chapters[chapter-1][1], - 'chapter': chapters[chapter-1][2], - 'verse_count': chapters[chapter-1][3] + 'id': chapters[chapter - 1][0], + 'book_reference_id': chapters[chapter - 1][1], + 'chapter': chapters[chapter - 1][2], + 'verse_count': chapters[chapter - 1][3] } except (IndexError, TypeError): return None @@ -846,13 +853,13 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager): file_path = os.path.join( AppLocation.get_directory(AppLocation.DataDir), 'bibles', 'alternative_book_names.sqlite') if not os.path.exists(file_path): - #create new DB, create table alternative_book_names + # create new DB, create table alternative_book_names AlternativeBookNamesDB.conn = sqlite3.connect(file_path) AlternativeBookNamesDB.conn.execute( 'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, ' 'book_reference_id INTEGER, language_id INTEGER, name VARCHAR(50), PRIMARY KEY (id))') else: - #use existing DB + # use existing DB AlternativeBookNamesDB.conn = sqlite3.connect(file_path) AlternativeBookNamesDB.cursor = AlternativeBookNamesDB.conn.cursor() return AlternativeBookNamesDB.cursor diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 340d8ef92..b15e11738 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -552,10 +552,10 @@ class HTTPBible(BibleDB, RegistryProperties): self.application.set_busy_cursor() search_results = self.get_chapter(book, reference[1]) if search_results and search_results.has_verse_list(): - ## We have found a book of the bible lets check to see - ## if it was there. By reusing the returned book name - ## we get a correct book. For example it is possible - ## to request ac and get Acts back. + # We have found a book of the bible lets check to see + # if it was there. By reusing the returned book name + # we get a correct book. For example it is possible + # to request ac and get Acts back. book_name = search_results.book self.application.process_events() # Check to see if book/chapter exists. diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 36357356b..a2cb1b594 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -480,6 +480,10 @@ class BibleMediaItem(MediaManagerItem): self.reload_bibles() def on_delete_click(self): + """ + When the delete button is pressed + """ + bible = None if self.quickTab.isVisible(): bible = self.quickVersionComboBox.currentText() elif self.advancedTab.isVisible(): @@ -488,8 +492,9 @@ class BibleMediaItem(MediaManagerItem): if QtGui.QMessageBox.question( self, UiStrings().ConfirmDelete, translate('BiblesPlugin.MediaItem', 'Are you sure you want to completely delete "%s" Bible from ' - 'OpenLP?\n\nYou will need to re-import this Bible to use it again.') % bible, - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), + 'OpenLP?\n\nYou will need to re-import this Bible to use it ' + 'again.') % bible, + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: return self.plugin.manager.delete_bible(bible) @@ -752,7 +757,7 @@ class BibleMediaItem(MediaManagerItem): log.exception('The second_search_results does not have as many verses as the search_results.') break bible_text = '%s %d%s%d (%s, %s)' % (book, verse.chapter, verse_separator, verse.verse, version, - second_version) + second_version) else: bible_text = '%s %d%s%d (%s)' % (book, verse.chapter, verse_separator, verse.verse, version) bible_verse = QtGui.QListWidgetItem(bible_text) diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index a522c1c57..c7bfa01a2 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -73,13 +73,13 @@ class OpenSongBible(BibleDB): log.debug('Starting OpenSong import from "%s"' % self.filename) if not isinstance(self.filename, str): self.filename = str(self.filename, 'utf8') - file = None + import_file = None success = True try: # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding # detection, and the two mechanisms together interfere with each other. - file = open(self.filename, 'r') - opensong = objectify.parse(file) + import_file = open(self.filename, 'rb') + opensong = objectify.parse(import_file) bible = opensong.getroot() language_id = self.get_language(bible_name) if not language_id: @@ -93,7 +93,7 @@ class OpenSongBible(BibleDB): log.error('Importing books from "%s" failed' % self.filename) return False book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - db_book = self.create_book(str(book.attrib['n']), book_ref_id, book_details['testament_id']) + db_book = self.create_book(book.attrib['n'], book_ref_id, book_details['testament_id']) chapter_number = 0 for chapter in book.c: if self.stop_import_flag: @@ -122,8 +122,8 @@ class OpenSongBible(BibleDB): verse_number += 1 self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse)) self.wizard.increment_progress_bar( - translate('BiblesPlugin.Opensong', 'Importing %s %s...', - 'Importing ...')) % (db_book.name, chapter_number) + translate('BiblesPlugin.Opensong', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) self.session.commit() self.application.process_events() except etree.XMLSyntaxError as inst: @@ -137,8 +137,8 @@ class OpenSongBible(BibleDB): log.exception('Loading Bible from OpenSong file failed') success = False finally: - if file: - file.close() + if import_file: + import_file.close() if self.stop_import_flag: return False else: diff --git a/openlp/plugins/bibles/lib/versereferencelist.py b/openlp/plugins/bibles/lib/versereferencelist.py index 1d3877d1d..7cbba68fb 100644 --- a/openlp/plugins/bibles/lib/versereferencelist.py +++ b/openlp/plugins/bibles/lib/versereferencelist.py @@ -94,5 +94,5 @@ class VerseReferenceList(object): result = result + ', ' + version['permission'] result = result.rstrip() if result.endswith(','): - return result[:len(result)-1] + return result[:len(result) - 1] return result diff --git a/openlp/plugins/custom/customplugin.py b/openlp/plugins/custom/customplugin.py index eb7de530a..2065717cb 100644 --- a/openlp/plugins/custom/customplugin.py +++ b/openlp/plugins/custom/customplugin.py @@ -43,7 +43,7 @@ log = logging.getLogger(__name__) __default_settings__ = { 'custom/db type': 'sqlite', - 'custom/last search type': CustomSearch.Titles, + 'custom/last search type': CustomSearch.Titles, 'custom/display footer': True, 'custom/add custom from service': True } @@ -97,12 +97,12 @@ class CustomPlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('CustomPlugin', 'Custom Slide', 'name singular'), 'plural': translate('CustomPlugin', 'Custom Slides', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('CustomPlugin', 'Custom Slides', 'container title') } diff --git a/openlp/plugins/custom/forms/editcustomslideform.py b/openlp/plugins/custom/forms/editcustomslideform.py index afce4887b..e6834dafb 100644 --- a/openlp/plugins/custom/forms/editcustomslideform.py +++ b/openlp/plugins/custom/forms/editcustomslideform.py @@ -1,4 +1,3 @@ -#lint:disable # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 diff --git a/openlp/plugins/custom/lib/customxmlhandler.py b/openlp/plugins/custom/lib/customxmlhandler.py index ca42a705b..3e1880055 100644 --- a/openlp/plugins/custom/lib/customxmlhandler.py +++ b/openlp/plugins/custom/lib/customxmlhandler.py @@ -51,7 +51,7 @@ from lxml import etree, objectify log = logging.getLogger(__name__) -#TODO: These classes need to be refactored into a single class. +# TODO: These classes need to be refactored into a single class. class CustomXMLBuilder(object): """ This class builds the XML used to describe songs. diff --git a/openlp/plugins/images/imageplugin.py b/openlp/plugins/images/imageplugin.py index 693449530..08c31f53c 100644 --- a/openlp/plugins/images/imageplugin.py +++ b/openlp/plugins/images/imageplugin.py @@ -95,12 +95,12 @@ class ImagePlugin(Plugin): """ Called to define all translatable texts of the plugin. """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('ImagePlugin', 'Image', 'name singular'), 'plural': translate('ImagePlugin', 'Images', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = {'title': translate('ImagePlugin', 'Images', 'container title')} # Middle Header Bar tooltips = { diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index d24a18434..c28f1e834 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -75,7 +75,7 @@ class ImageMediaItem(MediaManagerItem): def retranslateUi(self): self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)') file_formats = get_images_filter() - self.on_new_file_masks = '%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles) + self.on_new_file_masks = '%s;;%s (*)' % (file_formats, UiStrings().AllFiles) self.add_group_action.setText(UiStrings().AddGroup) self.add_group_action.setToolTip(UiStrings().AddGroup) self.replace_action.setText(UiStrings().ReplaceBG) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 5e75d1477..87d8e3311 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -95,7 +95,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.reset_action.setToolTip(UiStrings().ResetLiveBG) self.automatic = UiStrings().Automatic self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:')) - #self.rebuild_players() def required_icons(self): """ @@ -141,7 +140,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if index == 0: set_media_players(player) else: - set_media_players(player, player[index-1]) + set_media_players(player, player[index - 1]) def on_reset_click(self): """ diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 6cf241d1d..d0e082f53 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -73,12 +73,12 @@ class MediaPlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('MediaPlugin', 'Media', 'name singular'), 'plural': translate('MediaPlugin', 'Media', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('MediaPlugin', 'Media', 'container title') } diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index c55873c5f..584c1401f 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -377,8 +377,6 @@ class ImpressDocument(PresentationDocument): Stop the presentation, remove from screen. """ log.debug('stop presentation OpenOffice') - # deactivate should hide the screen according to docs, but doesn't - #self.control.deactivate() self.presentation.end() self.control = None diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 597b7d78b..5ed11d2ab 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -74,7 +74,7 @@ class PdfController(PresentationController): runlog = '' log.debug('testing program_path: %s', program_path) try: - runlog = check_output([program_path, '--help'], stderr=STDOUT) + runlog = check_output([program_path, '--help'], stderr=STDOUT) except CalledProcessError as e: runlog = e.output except Exception: @@ -183,7 +183,7 @@ class PdfDocument(PresentationDocument): self.image_files = [] self.num_pages = -1 - def gs_get_resolution(self, size): + def gs_get_resolution(self, size): """ Only used when using ghostscript Ghostscript can't scale automatically while keeping aspect like mupdf, so we need @@ -204,19 +204,19 @@ class PdfDocument(PresentationDocument): log.debug(' '.join(e.cmd)) log.debug(e.output) # Extract the pdf resolution from output, the format is " Size: x: , y: " - width = 0 - height = 0 + width = 0.0 + height = 0.0 for line in runlog.splitlines(): try: - width = int(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1)) - height = int(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1)) + width = float(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1)) + height = float(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1)) break except AttributeError: - pass + continue # Calculate the ratio from pdf to screen if width > 0 and height > 0: - width_ratio = size.right() / float(width) - height_ratio = size.bottom() / float(height) + width_ratio = size.right() / width + height_ratio = size.bottom() / height # return the resolution that should be used. 72 is default. if width_ratio > height_ratio: return int(height_ratio * 72) @@ -236,7 +236,7 @@ class PdfDocument(PresentationDocument): if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')): created_files = sorted(os.listdir(self.get_temp_folder())) for fn in created_files: - if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): + if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): self.image_files.append(os.path.join(self.get_temp_folder(), fn)) self.num_pages = len(self.image_files) return True diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index d6447adce..09fdf2fcc 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -247,7 +247,7 @@ class PowerpointDocument(PresentationDocument): Starts a presentation from the beginning. """ log.debug('start_presentation') - #SlideShowWindow measures its size/position by points, not pixels + # SlideShowWindow measures its size/position by points, not pixels try: dpi = win32ui.GetActiveWindow().GetDC().GetDeviceCaps(88) except win32ui.error: diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index fd636b468..6c8d7fb8c 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -122,8 +122,10 @@ class PresentationDocument(object): a file, e.g. thumbnails """ try: - shutil.rmtree(self.get_thumbnail_folder()) - shutil.rmtree(self.get_temp_folder()) + if os.path.exists(self.get_thumbnail_folder()): + shutil.rmtree(self.get_thumbnail_folder()) + if os.path.exists(self.get_temp_folder()): + shutil.rmtree(self.get_temp_folder()) except OSError: log.exception('Failed to delete presentation controller files') diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 7d5c81366..590289014 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -175,10 +175,10 @@ class PresentationTab(SettingsTab): if pdf_program == '': enable_pdf_program = 0 if pdf_program != Settings().value(self.settings_section + '/pdf_program'): - Settings().setValue(self.settings_section + '/pdf_program', pdf_program) + Settings().setValue(self.settings_section + '/pdf_program', pdf_program) changed = True if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'): - Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program) + Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program) changed = True if changed: self.settings_form.register_post_process('mediaitem_suffix_reset') diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index de0a0c5a5..5e0d7395d 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -156,12 +156,12 @@ class PresentationPlugin(Plugin): """ Called to define all translatable texts of the plugin. """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('PresentationPlugin', 'Presentation', 'name singular'), 'plural': translate('PresentationPlugin', 'Presentations', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('PresentationPlugin', 'Presentations', 'container title') } 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 393f08dd9..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): """ @@ -92,21 +108,23 @@ class RemotesPlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('RemotePlugin', 'Remote', 'name singular'), 'plural': translate('RemotePlugin', 'Remotes', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('RemotePlugin', 'Remote', 'container title') } 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/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 22299cde5..c411c8c1c 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -31,6 +31,7 @@ The duplicate song removal logic for OpenLP. """ import logging +import multiprocessing import os from PyQt4 import QtCore, QtGui @@ -45,6 +46,18 @@ from openlp.plugins.songs.lib.songcompare import songs_probably_equal log = logging.getLogger(__name__) +def song_generator(songs): + """ + This is a generator function to return tuples of two songs. When completed then all songs have once been returned + combined with any other songs. + + :param songs: All songs in the database. + """ + for outer_song_counter in range(len(songs) - 1): + for inner_song_counter in range(outer_song_counter + 1, len(songs)): + yield (songs[outer_song_counter], songs[inner_song_counter]) + + class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): """ This is the Duplicate Song Removal Wizard. It provides functionality to search for and remove duplicate songs @@ -114,7 +127,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): self.review_layout.addWidget(self.review_scroll_area) self.review_page_id = self.addPage(self.review_page) # Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the - #review page. + # review page. self.dummy_page = QtGui.QWizardPage() self.dummy_page_id = self.addPage(self.dummy_page) @@ -167,24 +180,31 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): max_progress_count = max_songs * (max_songs - 1) // 2 self.duplicate_search_progress_bar.setMaximum(max_progress_count) songs = self.plugin.manager.get_all_objects(Song) - for outer_song_counter in range(max_songs - 1): - for inner_song_counter in range(outer_song_counter + 1, max_songs): - if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): - duplicate_added = self.add_duplicates_to_song_list( - songs[outer_song_counter], songs[inner_song_counter]) - if duplicate_added: - self.found_duplicates_edit.appendPlainText( - songs[outer_song_counter].title + " = " + songs[inner_song_counter].title) - self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) - # The call to process_events() will keep the GUI responsive. - self.application.process_events() - if self.break_search: - return + # Create a worker/process pool to check the songs. + process_number = max(1, multiprocessing.cpu_count() - 1) + pool = multiprocessing.Pool(process_number) + result = pool.imap_unordered(songs_probably_equal, song_generator(songs), 30) + # Do not accept any further tasks. Also this closes the processes if all tasks are done. + pool.close() + # While the processes are still working, start to look at the results. + for song_tuple in result: + self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) + # The call to process_events() will keep the GUI responsive. + self.application.process_events() + if self.break_search: + pool.terminate() + return + if song_tuple is None: + continue + song1, song2 = song_tuple + duplicate_added = self.add_duplicates_to_song_list(song1, song2) + if duplicate_added: + self.found_duplicates_edit.appendPlainText(song1.title + " = " + song2.title) self.review_total_count = len(self.duplicate_song_list) - if self.review_total_count == 0: - self.notify_no_duplicates() - else: + if self.duplicate_song_list: self.button(QtGui.QWizard.NextButton).show() + else: + self.notify_no_duplicates() finally: self.application.set_normal_cursor() elif page_id == self.review_page_id: @@ -217,12 +237,12 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties): duplicate_added = False for duplicate_group in self.duplicate_song_list: # Skip the first song in the duplicate lists, since the first one has to be an earlier song. - if search_song in duplicate_group and not duplicate_song in duplicate_group: + if search_song in duplicate_group and duplicate_song not in duplicate_group: duplicate_group.append(duplicate_song) duplicate_group_found = True duplicate_added = True break - elif not search_song in duplicate_group and duplicate_song in duplicate_group: + elif search_song not in duplicate_group and duplicate_song in duplicate_group: duplicate_group.append(search_song) duplicate_group_found = True duplicate_added = True @@ -244,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..d0fb51a2d 100644 --- a/openlp/plugins/songs/forms/editsongdialog.py +++ b/openlp/plugins/songs/forms/editsongdialog.py @@ -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 b655c0f73..1814655ea 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 @@ -122,12 +122,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 +217,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()) @@ -302,6 +302,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 +463,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 +503,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 +521,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 +529,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( @@ -675,14 +683,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): separator = parts.find(':') if separator >= 0: verse_name = parts[0:separator].strip() - verse_num = parts[separator+1:].strip() + verse_num = parts[separator + 1:].strip() else: verse_name = parts verse_num = '1' verse_index = VerseType.from_loose_input(verse_name) verse_tag = VerseType.tags[verse_index] # Later we need to handle v1a as well. - #regex = re.compile(r'(\d+\w.)') regex = re.compile(r'\D*(\d+)\D*') match = regex.match(verse_num) if match: @@ -906,13 +913,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/editverseform.py b/openlp/plugins/songs/forms/editverseform.py index 038cff539..79a69a015 100644 --- a/openlp/plugins/songs/forms/editverseform.py +++ b/openlp/plugins/songs/forms/editverseform.py @@ -75,7 +75,7 @@ class EditVerseForm(QtGui.QDialog, Ui_EditVerseDialog): text = self.verse_text_edit.toPlainText() position = self.verse_text_edit.textCursor().position() insert_string = '[---]' - if position and text[position-1] != '\n': + if position and text[position - 1] != '\n': insert_string = '\n' + insert_string if position == len(text) or text[position] != '\n': insert_string += '\n' diff --git a/openlp/plugins/songs/forms/mediafilesdialog.py b/openlp/plugins/songs/forms/mediafilesdialog.py index a0392e3db..3a1db39ed 100644 --- a/openlp/plugins/songs/forms/mediafilesdialog.py +++ b/openlp/plugins/songs/forms/mediafilesdialog.py @@ -66,8 +66,10 @@ class Ui_MediaFilesDialog(object): def retranslateUi(self, media_files_dialog): """ Translate the UI on the fly. + + :param media_files_dialog: """ media_files_dialog.setWindowTitle(translate('SongsPlugin.MediaFilesForm', 'Select Media File(s)')) - self.select_label.setText(translate('SongsPlugin.MediaFilesForm', - 'Select one or more audio files from the list below, and click OK to import them ' - 'into this song.')) + self.select_label.setText(translate('SongsPlugin.MediaFilesForm', 'Select one or more audio files from the ' + 'list below, and click OK to import them ' + 'into this song.')) diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 4e345b750..589da4d33 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -152,9 +152,9 @@ class SongExportForm(OpenLPWizard): self.setWindowTitle(translate('SongsPlugin.ExportWizardForm', 'Song Export Wizard')) self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui', 'Welcome to the Song Export Wizard')) - self.information_label.setText(translate('SongsPlugin.ExportWizardForm', 'This wizard will help to' - ' export your songs to the open and free OpenLyrics worship ' - 'song format.')) + self.information_label.setText( + translate('SongsPlugin.ExportWizardForm', 'This wizard will help to export your songs to the open and free ' + 'OpenLyrics worship song format.')) self.available_songs_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Songs')) self.available_songs_page.setSubTitle(translate('SongsPlugin.ExportWizardForm', 'Check the songs you want to export.')) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 27f0d9343..21569a034 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -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/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index dc198d4b7..aa9fbc4c9 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -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: diff --git a/openlp/plugins/songs/lib/cclifileimport.py b/openlp/plugins/songs/lib/cclifileimport.py index 0866a1cc7..5c1fbdcb2 100644 --- a/openlp/plugins/songs/lib/cclifileimport.py +++ b/openlp/plugins/songs/lib/cclifileimport.py @@ -270,13 +270,13 @@ class CCLIFileImport(SongImport): verse_text = '' verse_start = False else: - #line_number=0, song title + # line_number=0, song title if line_number == 0: self.title = clean_line line_number += 1 - #line_number=1, verses + # line_number=1, verses elif line_number == 1: - #line_number=1, ccli number, first line after verses + # line_number=1, ccli number, first line after verses if clean_line.startswith('CCLI'): line_number += 1 ccli_parts = clean_line.split(' ') @@ -319,21 +319,21 @@ class CCLIFileImport(SongImport): # last part. Add l so as to keep the CRLF verse_text = verse_text + line else: - #line_number=2, copyright + # line_number=2, copyright if line_number == 2: line_number += 1 if clean_line.startswith('©'): self.copyright = clean_line else: song_author = clean_line - #n=3, authors + # n=3, authors elif line_number == 3: line_number += 1 if song_author: self.copyright = clean_line else: song_author = clean_line - #line_number=4, comments lines before last line + # line_number=4, comments lines before last line elif line_number == 4 and not clean_line.startswith('CCL'): self.comments += clean_line # split on known separators 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/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index cde0b1692..faa4122c8 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]+') @@ -77,9 +77,121 @@ class EasyWorshipSongImport(SongImport): 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 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') @@ -176,7 +288,6 @@ class EasyWorshipSongImport(SongImport): 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() if admin: @@ -187,55 +298,11 @@ class EasyWorshipSongImport(SongImport): if ccli: self.ccli_number = ccli.decode() 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() + else: + authors = '' + # Set the SongImport object members. + self.set_song_import_object(authors, words) if self.stop_import_flag: break if not self.finish(): @@ -243,12 +310,69 @@ class EasyWorshipSongImport(SongImport): 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 = 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]')) + 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 +409,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] @@ -305,7 +429,7 @@ class EasyWorshipSongImport(SongImport): elif field_desc.field_type == FieldType.Logical: return field ^ 0x80 == 1 elif field_desc.field_type == FieldType.Memo or field_desc.field_type == FieldType.Blob: - block_start, blob_size = struct.unpack_from('`` - 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 @@ -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): """ @@ -675,6 +687,7 @@ class OpenLyrics(object): sxml = SongXML() verses = {} verse_def_list = [] + verse_order = self._text(properties.verseOrder).split(' ') if hasattr(properties, 'verseOrder') else [] try: lyrics = song_xml.lyrics except AttributeError: @@ -717,13 +730,17 @@ class OpenLyrics(object): else: verses[(verse_tag, verse_number, lang, translit, verse_part)] = text verse_def_list.append((verse_tag, verse_number, lang, translit, verse_part)) + # Update verse order when the verse name has changed + if verse_def != verse_tag + verse_number + verse_part: + for i in range(len(verse_order)): + if verse_order[i] == verse_def: + verse_order[i] = verse_tag + verse_number + verse_part # We have to use a list to keep the order, as dicts are not sorted. for verse in verse_def_list: sxml.add_verse_to_lyrics(verse[0], verse[1], verses[verse], verse[2]) song_obj.lyrics = str(sxml.extract_xml(), 'utf-8') # Process verse order - if hasattr(properties, 'verseOrder'): - song_obj.verse_order = self._text(properties.verseOrder) + song_obj.verse_order = ' '.join(verse_order) def _process_songbooks(self, properties, song): """ diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 1c000338c..b1ddaf412 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -132,7 +132,7 @@ class SongsPlugin(Plugin): ) import_menu.addAction(self.import_songselect_item) - def add_export_menu_Item(self, export_menu): + def add_export_menu_item(self, export_menu): """ Give the Songs plugin the opportunity to add items to the **Export** menu. @@ -261,12 +261,12 @@ class SongsPlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('SongsPlugin', 'Song', 'name singular'), 'plural': translate('SongsPlugin', 'Songs', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('SongsPlugin', 'Songs', 'container title') } diff --git a/openlp/plugins/songusage/songusageplugin.py b/openlp/plugins/songusage/songusageplugin.py index 4d38de6dd..8c0fc5762 100644 --- a/openlp/plugins/songusage/songusageplugin.py +++ b/openlp/plugins/songusage/songusageplugin.py @@ -246,12 +246,12 @@ class SongUsagePlugin(Plugin): """ Called to define all translatable texts of the plugin """ - ## Name PluginList ## + # Name PluginList self.text_strings[StringContent.Name] = { 'singular': translate('SongUsagePlugin', 'SongUsage', 'name singular'), 'plural': translate('SongUsagePlugin', 'SongUsage', 'name plural') } - ## Name for MediaDockManager, SettingsManager ## + # Name for MediaDockManager, SettingsManager self.text_strings[StringContent.VisibleName] = { 'title': translate('SongUsagePlugin', 'SongUsage', 'container title') } 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 6af0e77a5..79036f08f 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -149,6 +149,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 386ab69ef..eeafbfe23 100644 --- a/scripts/jenkins_script.py +++ b/scripts/jenkins_script.py @@ -40,6 +40,7 @@ You can look up the token in the Branch-01-Pull job configuration or ask in IRC. """ from optparse import OptionParser +import re from requests.exceptions import HTTPError from subprocess import Popen, PIPE import sys @@ -49,6 +50,9 @@ from jenkins import Jenkins JENKINS_URL = 'http://ci.openlp.org/' +REPO_REGEX = r'(.*/+)(~.*)' +# Allows us to black list token. So when we change the token, we can display a proper message to the user. +OLD_TOKENS = [] class OpenLPJobs(object): @@ -59,9 +63,20 @@ class OpenLPJobs(object): Branch_Functional = 'Branch-02-Functional-Tests' Branch_Interface = 'Branch-03-Interface-Tests' Branch_Windows = 'Branch-04-Windows_Tests' - Branch_PEP = 'Branch-05-Code-Analysis' + Branch_PEP = 'Branch-05a-Code_Analysis' + Branch_Coverage = 'Branch-05b-Test_Coverage' - Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows, Branch_PEP] + Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows, Branch_PEP, Branch_Coverage] + + +class Colour(object): + """ + This class holds values which can be used to print coloured text. + """ + RED_START = '\033[1;31m' + RED_END = '\033[1;m' + GREEN_START = '\033[1;32m' + GREEN_END = '\033[1;m' class JenkinsTrigger(object): @@ -79,14 +94,25 @@ class JenkinsTrigger(object): """ Ask our jenkins server to build the "Branch-01-Pull" job. """ - self.jenkins_instance.job(OpenLPJobs.Branch_Pull).build({'BRANCH_NAME': self.repo_name}, token=self.token) + bzr = Popen(('bzr', 'whoami'), stdout=PIPE, stderr=PIPE) + raw_output, error = bzr.communicate() + # We just want the name (not the email). + name = ' '.join(raw_output.decode().split()[:-1]) + cause = 'Build triggered by %s (%s)' % (name, self.repo_name) + self.jenkins_instance.job(OpenLPJobs.Branch_Pull).build( + {'BRANCH_NAME': self.repo_name, 'cause': cause}, token=self.token) def print_output(self): """ Print the status information of the build tirggered. """ - print("Add this to your merge proposal:") - print("--------------------------------") + print('Add this to your merge proposal:') + print('--------------------------------') + bzr = Popen(('bzr', 'revno'), stdout=PIPE, stderr=PIPE) + raw_output, error = bzr.communicate() + revno = raw_output.decode().strip() + print('%s (revision %s)' % (get_repo_name(), revno)) + for job in OpenLPJobs.Jobs: self.__print_build_info(job) @@ -107,22 +133,22 @@ class JenkinsTrigger(object): """ job = self.jenkins_instance.job(job_name) while job.info['inQueue']: - # Give other processes the possibility to take over. Like Thread.yield(). - time.sleep(0) + time.sleep(1) build = job.last_build build.wait() - result_string = build.info['result'] + if build.info['result'] == 'SUCCESS': + # Make 'SUCCESS' green. + result_string = '%s%s%s' % (Colour.GREEN_START, build.info['result'], Colour.GREEN_END) + else: + # Make 'FAILURE' red. + result_string = '%s%s%s' % (Colour.RED_START, build.info['result'], Colour.RED_END) url = build.info['url'] print('[%s] %s' % (result_string, url)) - # On failure open the browser. - #if result_string == "FAILURE": - # url += 'console' - # Popen(('xdg-open', url), stderr=PIPE) 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) @@ -139,46 +165,41 @@ def get_repo_name(): for line in output_list: # Check if it is remote branch. if 'push branch' in line: - repo_name = line.replace('push branch: bzr+ssh://bazaar.launchpad.net/', 'lp:') - break + match = re.match(REPO_REGEX, line) + if match: + repo_name = 'lp:%s' % match.group(2) + break elif 'checkout of branch' in line: - repo_name = line.replace('checkout of branch: bzr+ssh://bazaar.launchpad.net/', 'lp:') - break - repo_name = repo_name.strip('/') - - # Did we find the branch name? - if not repo_name: - for line in output_list: - # Check if the branch was pushed. - if 'Shared repository with trees (format: 2a)' in line: - print('Not a branch. cd to a branch.') - return - print('Not a branch. Have you pushed it to launchpad?') - return - return repo_name + match = re.match(REPO_REGEX, line) + if match: + repo_name = 'lp:%s' % match.group(2) + break + return repo_name.strip('/') def main(): usage = 'Usage: python %prog TOKEN [options]' parser = OptionParser(usage=usage) - parser.add_option('-d', '--disable-output', dest='enable_output', action="store_false", default=True, + parser.add_option('-d', '--disable-output', dest='enable_output', action='store_false', default=True, help='Disable output.') - parser.add_option('-b', '--open-browser', dest='open_browser', action="store_true", default=False, + parser.add_option('-b', '--open-browser', dest='open_browser', action='store_true', default=False, help='Opens the jenkins page in your browser.') - #parser.add_option('-e', '--open-browser-on-error', dest='open_browser_on_error', action="store_true", - # default=False, help='Opens the jenkins page in your browser in case a test fails.') options, args = parser.parse_args(sys.argv) if len(args) == 2: if not get_repo_name(): + print('Not a branch. Have you pushed it to launchpad? Did you cd to the branch?') return token = args[-1] + if token in OLD_TOKENS: + print('Your token is not valid anymore. Get the most recent one.') + return jenkins_trigger = JenkinsTrigger(token) try: jenkins_trigger.trigger_build() - except HTTPError as e: - print("Wrong token.") + except HTTPError: + print('Wrong token.') return # Open the browser before printing the output. if options.open_browser: diff --git a/scripts/translation_utils.py b/scripts/translation_utils.py index 5aa320806..ad3edcaa3 100755 --- a/scripts/translation_utils.py +++ b/scripts/translation_utils.py @@ -96,7 +96,7 @@ class CommandStack(object): return len(self.data) def __getitem__(self, index): - if not index in self.data: + if index not in self.data: return None elif self.data[index].get('arguments'): return self.data[index]['command'], self.data[index]['arguments'] diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_common.py similarity index 76% rename from tests/functional/openlp_core_common/test_init.py rename to tests/functional/openlp_core_common/test_common.py index dcc4fe32a..ab2d11b3a 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_common.py @@ -32,12 +32,13 @@ Functional tests to test the AppLocation class and related methods. from unittest import TestCase -from openlp.core.common import de_hump +from openlp.core.common import de_hump, trace_error_handler +from tests.functional import MagicMock, patch -class TestInitFunctions(TestCase): +class TestCommonFunctions(TestCase): """ - A test suite to test out various functions in the __init__ class. + A test suite to test out various functions in the openlp.core.common module. """ def de_hump_conversion_test(self): """ @@ -64,3 +65,19 @@ class TestInitFunctions(TestCase): # THEN: the new string should be converted to python format self.assertTrue(new_string == "my_class", 'The class name should have been preserved') + + def trace_error_handler_test(self): + """ + Test the trace_error_handler() method + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.traceback') as mocked_traceback: + mocked_traceback.extract_stack.return_value = [('openlp.fake', 56, None, 'trace_error_handler_test')] + mocked_logger = MagicMock() + + # WHEN: trace_error_handler() is called + trace_error_handler(mocked_logger) + + # THEN: The mocked_logger.error() method should have been called with the correct parameters + mocked_logger.error.assert_called_with( + 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') diff --git a/tests/functional/openlp_core_common/test_registryproperties.py b/tests/functional/openlp_core_common/test_registryproperties.py index fa8a2b540..c2e430a95 100644 --- a/tests/functional/openlp_core_common/test_registryproperties.py +++ b/tests/functional/openlp_core_common/test_registryproperties.py @@ -52,7 +52,7 @@ class TestRegistryProperties(TestCase, RegistryProperties): # GIVEN an Empty Registry # WHEN there is no Application # THEN the application should be none - self.assertEquals(self.application, None, 'The application value should be None') + self.assertEqual(self.application, None, 'The application value should be None') def application_test(self): """ @@ -63,4 +63,4 @@ class TestRegistryProperties(TestCase, RegistryProperties): # WHEN the application is registered Registry().register('application', application) # THEN the application should be none - self.assertEquals(self.application, application, 'The application value should match') + self.assertEqual(self.application, application, 'The application value should match') diff --git a/tests/functional/openlp_core_lib/test_db.py b/tests/functional/openlp_core_lib/test_db.py index f11292209..071a3904b 100644 --- a/tests/functional/openlp_core_lib/test_db.py +++ b/tests/functional/openlp_core_lib/test_db.py @@ -50,8 +50,8 @@ class TestDB(TestCase): """ # GIVEN: Mocked out SQLAlchemy calls and return objects, and an in-memory SQLite database URL with patch('openlp.core.lib.db.create_engine') as mocked_create_engine, \ - patch('openlp.core.lib.db.MetaData') as MockedMetaData, \ - patch('openlp.core.lib.db.sessionmaker') as mocked_sessionmaker, \ + patch('openlp.core.lib.db.MetaData') as MockedMetaData, \ + patch('openlp.core.lib.db.sessionmaker') as mocked_sessionmaker, \ patch('openlp.core.lib.db.scoped_session') as mocked_scoped_session: mocked_engine = MagicMock() mocked_metadata = MagicMock() diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py index b2c2178a9..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 e9968e1c7..37b6d6fdd 100644 --- a/tests/functional/openlp_core_lib/test_image_manager.py +++ b/tests/functional/openlp_core_lib/test_image_manager.py @@ -30,12 +30,16 @@ Package to test the openlp.core.ui package. """ import os +import time +from threading import Lock from unittest import TestCase from PyQt4 import QtGui from openlp.core.common import Registry from openlp.core.lib import ImageManager, ScreenList +from openlp.core.lib.imagemanager import Priority +from tests.functional import patch from tests.helpers.testmixin import TestMixin TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) @@ -51,6 +55,8 @@ class TestImageManager(TestCase, TestMixin): self.get_application() ScreenList.create(self.app.desktop()) self.image_manager = ImageManager() + self.lock = Lock() + self.sleep_time = 0.1 def tearDown(self): """ @@ -82,3 +88,87 @@ class TestImageManager(TestCase, TestMixin): with self.assertRaises(KeyError) as context: self.image_manager.get_image(TEST_PATH, 'church1.jpg') self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image') + + def process_cache_test(self): + """ + Test the process_cache method + """ + with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \ + patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte: + # GIVEN: Mocked functions + mocked_resize_image.side_effect = self.mocked_resize_image + mocked_image_to_byte.side_effect = self.mocked_image_to_byte + image1 = 'church.jpg' + image2 = 'church2.jpg' + image3 = 'church3.jpg' + image4 = 'church4.jpg' + + # WHEN: Add the images. Then get the lock (=queue can not be processed). + self.lock.acquire() + self.image_manager.add_image(TEST_PATH, image1, None) + self.image_manager.add_image(TEST_PATH, image2, None) + + # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but + # is being processed (see mocked methods/functions). + # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the # + # priority is adjusted to Priority.Lowest). + self.assertEqual(self.get_image_priority(image1), Priority.Normal, + "image1's priority should be 'Priority.Normal'") + self.assertEqual(self.get_image_priority(image2), Priority.Normal, + "image2's priority should be 'Priority.Normal'") + + # WHEN: Add more images. + self.image_manager.add_image(TEST_PATH, image3, None) + self.image_manager.add_image(TEST_PATH, image4, None) + # Allow the queue to process. + self.lock.release() + # Request some "data". + image_bytes = self.image_manager.get_image_bytes(TEST_PATH, image4) + image_object = self.image_manager.get_image(TEST_PATH, image3) + # Now the mocked methods/functions do not have to sleep anymore. + self.sleep_time = 0 + # Wait for the queue to finish. + while not self.image_manager._conversion_queue.empty(): + time.sleep(0.1) + # Because empty() is not reliable, wait a litte; just to make sure. + time.sleep(0.1) + # THEN: The images' priority reflect how they were processed. + self.assertEqual(self.image_manager._conversion_queue.qsize(), 0, "The queue should be empty.") + self.assertEqual(self.get_image_priority(image1), Priority.Lowest, + "The image should have not been requested (=Lowest)") + self.assertEqual(self.get_image_priority(image2), Priority.Lowest, + "The image should have not been requested (=Lowest)") + self.assertEqual(self.get_image_priority(image3), Priority.Low, + "Only the QImage should have been requested (=Low).") + self.assertEqual(self.get_image_priority(image4), Priority.Urgent, + "The image bytes should have been requested (=Urgent).") + + def get_image_priority(self, image): + """ + This is a help method to get the priority of the given image out of the image_manager's cache. + + NOTE: This requires, that the image has been added to the image manager using the *TEST_PATH*. + + :param image: The name of the image. E. g. ``image1`` + """ + return self.image_manager._cache[(TEST_PATH, image)].priority + + def mocked_resize_image(self, *args): + """ + This is a mocked method, so that we can control the work flow of the image manager. + """ + self.lock.acquire() + self.lock.release() + # The sleep time is adjusted in the test case. + time.sleep(self.sleep_time) + return QtGui.QImage() + + def mocked_image_to_byte(self, *args): + """ + This is a mocked method, so that we can control the work flow of the image manager. + """ + self.lock.acquire() + self.lock.release() + # The sleep time is adjusted in the test case. + time.sleep(self.sleep_time) + return '' \ No newline at end of file diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index bb3a17ebb..b4334a728 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -482,7 +482,7 @@ class TestLib(TestCase): # WHEN: we run the validate_thumb() function # THEN: we should have called a few functions, and the result should be True - #mocked_os.path.exists.assert_called_with(thumb_path) + # mocked_os.path.exists.assert_called_with(thumb_path) def validate_thumb_file_exists_and_older_test(self): """ diff --git a/tests/functional/openlp_core_lib/test_pluginmanager.py b/tests/functional/openlp_core_lib/test_pluginmanager.py index ce5c240b2..d98a522b8 100644 --- a/tests/functional/openlp_core_lib/test_pluginmanager.py +++ b/tests/functional/openlp_core_lib/test_pluginmanager.py @@ -212,9 +212,9 @@ class TestPluginManager(TestCase): # WHEN: We run hook_export_menu() plugin_manager.hook_export_menu() - # THEN: The add_export_menu_Item() method should not have been called - self.assertEqual(0, mocked_plugin.add_export_menu_Item.call_count, - 'The add_export_menu_Item() method should not have been called.') + # THEN: The add_export_menu_item() method should not have been called + self.assertEqual(0, mocked_plugin.add_export_menu_item.call_count, + 'The add_export_menu_item() method should not have been called.') def hook_export_menu_with_active_plugin_test(self): """ @@ -229,8 +229,8 @@ class TestPluginManager(TestCase): # WHEN: We run hook_export_menu() plugin_manager.hook_export_menu() - # THEN: The add_export_menu_Item() method should have been called - mocked_plugin.add_export_menu_Item.assert_called_with(self.mocked_main_window.file_export_menu) + # THEN: The add_export_menu_item() method should have been called + mocked_plugin.add_export_menu_item.assert_called_with(self.mocked_main_window.file_export_menu) def hook_upgrade_plugin_settings_with_disabled_plugin_test(self): """ @@ -264,7 +264,7 @@ class TestPluginManager(TestCase): # WHEN: We run hook_upgrade_plugin_settings() plugin_manager.hook_upgrade_plugin_settings(settings) - # THEN: The add_export_menu_Item() method should have been called + # THEN: The add_export_menu_item() method should have been called mocked_plugin.upgrade_settings.assert_called_with(settings) def hook_tools_menu_with_disabled_plugin_test(self): diff --git a/tests/functional/openlp_core_lib/test_renderer.py b/tests/functional/openlp_core_lib/test_renderer.py index 51e915e3f..8814a21a0 100644 --- a/tests/functional/openlp_core_lib/test_renderer.py +++ b/tests/functional/openlp_core_lib/test_renderer.py @@ -49,7 +49,7 @@ class TestRenderer(TestCase): def setUp(self): """ - Set up the components need for all tests. + Set up the components need for all tests """ # Mocked out desktop object self.desktop = MagicMock() @@ -67,7 +67,7 @@ class TestRenderer(TestCase): def initial_renderer_test(self): """ - Test the initial renderer state . + Test the initial renderer state """ # GIVEN: A new renderer instance. renderer = Renderer() @@ -77,7 +77,7 @@ class TestRenderer(TestCase): def default_screen_layout_test(self): """ - Test the default layout calculations. + Test the default layout calculations """ # GIVEN: A new renderer instance. renderer = Renderer() @@ -87,3 +87,35 @@ class TestRenderer(TestCase): self.assertEqual(renderer.height, 768, 'The base renderer should be a live controller') self.assertEqual(renderer.screen_ratio, 0.75, 'The base renderer should be a live controller') self.assertEqual(renderer.footer_start, 691, 'The base renderer should be a live controller') + + def _get_start_tags_test(self): + """ + Test the _get_start_tags() method + """ + # GIVEN: A new renderer instance. Broken raw_text (missing closing tags). + renderer = Renderer() + given_raw_text = '{st}{r}Text text text' + expected_tuple = ('{st}{r}Text text text{/r}{/st}', '{st}{r}', + '') + + # WHEN: + result = renderer._get_start_tags(given_raw_text) + + # THEN: Check if the correct tuple is returned. + self.assertEqual(result, expected_tuple), 'A tuple should be returned containing the text with correct ' \ + 'tags, the opening tags, and the opening html tags.' + + def _word_split_test(self): + """ + Test the _word_split() method + """ + # GIVEN: A line of text + renderer = Renderer() + given_line = 'beginning asdf \n end asdf' + expected_words = ['beginning', 'asdf', 'end', 'asdf'] + + # WHEN: Split the line + result_words = renderer._words_split(given_line) + + # THEN: The word lists should be the same. + self.assertListEqual(result_words, expected_words) diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 03cdd5bb1..025b1a638 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -82,6 +82,38 @@ class TestUi(TestCase): self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) + def test_create_button(self): + """ + Test creating a button + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create the button + btn = create_button(dialog, 'my_btn') + + # THEN: We should get a button with a name + self.assertIsInstance(btn, QtGui.QPushButton) + self.assertEqual('my_btn', btn.objectName()) + self.assertTrue(btn.isEnabled()) + + # WHEN: We create a button with some attributes + btn = create_button(dialog, 'my_btn', text='Hello', tooltip='How are you?', enabled=False) + + # THEN: We should get a button with those attributes + self.assertIsInstance(btn, QtGui.QPushButton) + self.assertEqual('Hello', btn.text()) + self.assertEqual('How are you?', btn.toolTip()) + self.assertFalse(btn.isEnabled()) + + # WHEN: We create a toolbutton + btn = create_button(dialog, 'my_btn', btn_class='toolbutton') + + # THEN: We should get a toolbutton + self.assertIsInstance(btn, QtGui.QToolButton) + 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 @@ -98,3 +130,43 @@ class TestUi(TestCase): 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 + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action + action = create_action(dialog, 'my_action') + + # THEN: We should get a QAction + self.assertIsInstance(action, QtGui.QAction) + self.assertEqual('my_action', action.objectName()) + + # WHEN: We create an action with some properties + action = create_action(dialog, 'my_action', text='my text', icon=':/wizards/wizard_firsttime.bmp', + tooltip='my tooltip', statustip='my statustip') + + # THEN: These properties should be set + self.assertIsInstance(action, QtGui.QAction) + self.assertEqual('my text', action.text()) + self.assertIsInstance(action.icon(), QtGui.QIcon) + self.assertEqual('my tooltip', action.toolTip()) + self.assertEqual('my statustip', action.statusTip()) diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py new file mode 100644 index 000000000..9fc6f5137 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -0,0 +1,72 @@ +# -*- 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.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 + + +class TestFirstTimeForm(TestCase, TestMixin): + + def setUp(self): + screens = MagicMock() + self.get_application() + Registry.create() + Registry().register('application', self.app) + self.first_time_form = FirstTimeForm(screens) + + def test_access_to_config(self): + """ + Test if we can access the First Time Form's config file + """ + # GIVEN A new First Time Form instance. + + # WHEN The default First Time Form is built. + + # THEN The First Time Form web configuration file should be accessable. + self.assertTrue(self.first_time_form.web_access, + 'First Time Wizard\'s web configuration file should be available') + + def test_parsable_config(self): + """ + Test if the First Time Form's config file is parsable + """ + # GIVEN A new First Time Form instance. + + # WHEN The default First Time Form is built. + + # THEN The First Time Form web configuration file should be parsable + self.assertTrue(self.first_time_form.songs_url, + 'First Time Wizard\'s web configuration file should be parsable') diff --git a/tests/functional/openlp_core_ui/test_formattingtagsform.py b/tests/functional/openlp_core_ui/test_formattingtagsform.py index 05b5fed74..e71a75651 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagsform.py +++ b/tests/functional/openlp_core_ui/test_formattingtagsform.py @@ -70,7 +70,7 @@ class TestFormattingTagForm(TestCase): form.save_button = MagicMock() # WHEN: on_text_edited is called with an arbitrary value - #form.on_text_edited('text') + # form.on_text_edited('text') # THEN: setEnabled and setDefault should have been called on save_push_button - #form.save_button.setEnabled.assert_called_with(True) + # form.save_button.setEnabled.assert_called_with(True) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index dffc6ec16..6d67a3b67 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -79,3 +79,31 @@ class TestMainDisplay(TestCase): # THEN: The controller should not be a live controller. self.assertEqual(main_display.is_live, True, 'The main display should be a live controller') + + def set_transparency_test(self): + """ + Test creating an instance of the MainDisplay class + """ + # GIVEN: get an instance of MainDisplay + display = MagicMock() + main_display = MainDisplay(display) + + # WHEN: We enable transparency + main_display.set_transparency(True) + + # THEN: There should be a Stylesheet + self.assertEqual('QGraphicsView {background: transparent; border: 0px;}', main_display.styleSheet(), + 'MainDisplay instance should be transparent') + self.assertFalse(main_display.autoFillBackground(), + 'MainDisplay instance should be without background auto fill') + self.assertTrue(main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground), + 'MainDisplay hasnt translucent background') + + # WHEN: We disable transparency + main_display.set_transparency(False) + + # THEN: The Stylesheet should be empty + self.assertEqual('QGraphicsView {}', main_display.styleSheet(), + 'MainDisplay instance should not be transparent') + self.assertFalse(main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground), + 'MainDisplay hasnt translucent background') diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py new file mode 100644 index 000000000..d59690949 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_media.py @@ -0,0 +1,128 @@ +# -*- 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 package. +""" +from PyQt4 import QtCore +from unittest import TestCase + +from openlp.core.ui.media import get_media_players + +from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin + + +class TestMedia(TestCase, TestMixin): + + def setUp(self): + pass + + def test_get_media_players_no_config(self): + """ + Test that when there's no config, get_media_players() returns an empty list of players (not a string) + """ + def value_results(key): + if key == 'media/players': + return '' + else: + return False + + # GIVEN: A mocked out Settings() object + with patch('openlp.core.ui.media.Settings.value') as mocked_value: + mocked_value.side_effect = value_results + + # WHEN: get_media_players() is called + used_players, overridden_player = get_media_players() + + # THEN: the used_players should be an empty list, and the overridden player should be an empty string + self.assertEqual([], used_players, 'Used players should be an empty list') + self.assertEqual('', overridden_player, 'Overridden player should be an empty string') + + def test_get_media_players_no_players(self): + """ + Test that when there's no players but overridden player is set, get_media_players() returns 'auto' + """ + def value_results(key): + if key == 'media/override player': + return QtCore.Qt.Checked + else: + return '' + + # GIVEN: A mocked out Settings() object + with patch('openlp.core.ui.media.Settings.value') as mocked_value: + mocked_value.side_effect = value_results + + # WHEN: get_media_players() is called + used_players, overridden_player = get_media_players() + + # THEN: the used_players should be an empty list, and the overridden player should be an empty string + self.assertEqual([], used_players, 'Used players should be an empty list') + self.assertEqual('auto', overridden_player, 'Overridden player should be "auto"') + + def test_get_media_players_with_valid_list(self): + """ + Test that when get_media_players() is called the string list is interpreted correctly + """ + def value_results(key): + if key == 'media/players': + return '[vlc,webkit,phonon]' + else: + return False + + # GIVEN: A mocked out Settings() object + with patch('openlp.core.ui.media.Settings.value') as mocked_value: + mocked_value.side_effect = value_results + + # WHEN: get_media_players() is called + used_players, overridden_player = get_media_players() + + # 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('', overridden_player, 'Overridden player should be an empty string') + + def test_get_media_players_with_overridden_player(self): + """ + Test that when get_media_players() is called the overridden player is correctly set + """ + def value_results(key): + if key == 'media/players': + return '[vlc,webkit,phonon]' + else: + return QtCore.Qt.Checked + + # GIVEN: A mocked out Settings() object + with patch('openlp.core.ui.media.Settings.value') as mocked_value: + mocked_value.side_effect = value_results + + # WHEN: get_media_players() is called + used_players, overridden_player = get_media_players() + + # 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 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_plugins/bibles/test_http.py b/tests/functional/openlp_plugins/bibles/test_http.py index b9bb8f11c..060c00d02 100644 --- a/tests/functional/openlp_plugins/bibles/test_http.py +++ b/tests/functional/openlp_plugins/bibles/test_http.py @@ -35,7 +35,7 @@ from bs4 import BeautifulSoup from tests.functional import patch, MagicMock from openlp.plugins.bibles.lib.http import BSExtract -#TODO: Items left to test +# TODO: Items left to test # BGExtract # __init__ # _remove_elements @@ -68,7 +68,7 @@ class TestBSExtract(TestCase): """ Test the BSExtractClass """ - #TODO: Items left to test + # TODO: Items left to test # BSExtract # __init__ # get_bible_chapter @@ -180,4 +180,4 @@ class TestBSExtract(TestCase): 'http://m.bibleserver.com/overlay/selectBook?translation=NIV') self.assertFalse(self.mock_log.error.called, 'log.error should not have been called') self.assertFalse(self.mock_send_error_message.called, 'send_error_message should not have been called') - self.assertEquals(result, ['Genesis', 'Leviticus']) + self.assertEqual(result, ['Genesis', 'Leviticus']) diff --git a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py index 8a8897cec..c3d0912c0 100644 --- a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py @@ -47,7 +47,7 @@ class TestPptviewController(TestCase, TestMixin): """ Test the PptviewController Class """ -#TODO: Items left to test +# TODO: Items left to test # PptviewController # start_process(self) # kill @@ -108,7 +108,7 @@ class TestPptviewDocument(TestCase): """ Test the PptviewDocument Class """ - #TODO: Items left to test + # TODO: Items left to test # PptviewDocument # __init__ # create_thumbnails diff --git a/tests/functional/openlp_plugins/songs/test_ewimport.py b/tests/functional/openlp_plugins/songs/test_ewimport.py index 9e327517c..49bc367cd 100644 --- a/tests/functional/openlp_plugins/songs/test_ewimport.py +++ b/tests/functional/openlp_plugins/songs/test_ewimport.py @@ -69,6 +69,20 @@ SONG_TEST_DATA = [ 'Just to bow and receive a new blessing,\nIn the beautiful garden of prayer.', '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): """ @@ -139,10 +153,10 @@ class TestEasyWorshipSongImport(TestCase): # THEN: self.assertIsNotNone(field_desc_entry, 'Import should not be none') - self.assertEquals(field_desc_entry.name, name, 'FieldDescEntry.name should be the same as the name argument') - self.assertEquals(field_desc_entry.field_type, field_type, - 'FieldDescEntry.type should be the same as the type argument') - self.assertEquals(field_desc_entry.size, size, 'FieldDescEntry.size should be the same as the size argument') + self.assertEqual(field_desc_entry.name, name, 'FieldDescEntry.name should be the same as the name argument') + self.assertEqual(field_desc_entry.field_type, field_type, + 'FieldDescEntry.type should be the same as the type argument') + self.assertEqual(field_desc_entry.size, size, 'FieldDescEntry.size should be the same as the size argument') def create_importer_test(self): """ @@ -174,7 +188,7 @@ class TestEasyWorshipSongImport(TestCase): for field_name in existing_fields: # THEN: The item corresponding the index returned should have the same name attribute - self.assertEquals(importer.field_descriptions[importer.find_field(field_name)].name, field_name) + self.assertEqual(importer.field_descriptions[importer.find_field(field_name)].name, field_name) def find_non_existing_field_test(self): """ @@ -229,10 +243,10 @@ class TestEasyWorshipSongImport(TestCase): for field_index, result in field_results: return_value = importer.get_field(field_index) - # THEN: get_field should return the known results - self.assertEquals(return_value, result, - 'get_field should return "%s" when called with "%s"' % - (result, TEST_FIELDS[field_index])) + # THEN: get_field should return the known results + self.assertEqual(return_value, result, + 'get_field should return "%s" when called with "%s"' % + (result, TEST_FIELDS[field_index])) def get_memo_field_test(self): """ @@ -257,7 +271,7 @@ class TestEasyWorshipSongImport(TestCase): get_field_seek_calls = test_results[2]['seek'] # THEN: get_field should return the appropriate value with the appropriate mocked objects being called - self.assertEquals(importer.get_field(field_index), get_field_result) + self.assertEqual(importer.get_field(field_index), get_field_result) for call in get_field_read_calls: mocked_memo_file.read.assert_any_call(call) for call in get_field_seek_calls: @@ -357,9 +371,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 +400,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'] @@ -403,11 +418,52 @@ class TestEasyWorshipSongImport(TestCase): if song_copyright: self.assertEqual(importer.copyright, song_copyright) if ccli_number: - self.assertEquals(importer.ccli_number, ccli_number, 'ccli_number for %s should be %s' - % (title, ccli_number)) + self.assertEqual(importer.ccli_number, ccli_number, + 'ccli_number for %s should be %s' % (title, ccli_number)) for verse_text, verse_tag in add_verse_calls: mocked_add_verse.assert_any_call(verse_text, verse_tag) if verse_order_list: - self.assertEquals(importer.verse_order_list, verse_order_list, - 'verse_order_list for %s should be %s' % (title, verse_order_list)) + 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() diff --git a/tests/functional/openlp_plugins/songs/test_foilpresenterimport.py b/tests/functional/openlp_plugins/songs/test_foilpresenterimport.py index fbd339cf3..61206b9fa 100644 --- a/tests/functional/openlp_plugins/songs/test_foilpresenterimport.py +++ b/tests/functional/openlp_plugins/songs/test_foilpresenterimport.py @@ -44,7 +44,7 @@ class TestFoilPresenter(TestCase): """ Test the functions in the :mod:`foilpresenterimport` module. """ - #TODO: The following modules still need tests written for + # TODO: The following modules still need tests written for # xml_to_song # _child # _process_authors diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index f6e5d98b9..b67c1a4be 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -96,10 +96,10 @@ class TestLib(TestCase): self.song2.search_lyrics = self.full_lyrics # WHEN: We compare those songs for equality. - result = songs_probably_equal(self.song1, self.song2) + result = songs_probably_equal((self.song1, self.song2)) - # THEN: The result should be True. - assert result is True, 'The result should be True' + # THEN: The result should be a tuple.. + assert result == (self.song1, self.song2), 'The result should be the tuble of songs' def songs_probably_equal_short_song_test(self): """ @@ -110,10 +110,10 @@ class TestLib(TestCase): self.song2.search_lyrics = self.short_lyrics # WHEN: We compare those songs for equality. - result = songs_probably_equal(self.song1, self.song2) + result = songs_probably_equal((self.song1, self.song2)) - # THEN: The result should be True. - assert result is True, 'The result should be True' + # THEN: The result should be a tuple.. + assert result == (self.song1, self.song2), 'The result should be the tuble of songs' def songs_probably_equal_error_song_test(self): """ @@ -124,10 +124,10 @@ class TestLib(TestCase): self.song2.search_lyrics = self.error_lyrics # WHEN: We compare those songs for equality. - result = songs_probably_equal(self.song1, self.song2) + result = songs_probably_equal((self.song1, self.song2)) - # THEN: The result should be True. - assert result is True, 'The result should be True' + # THEN: The result should be a tuple of songs.. + assert result == (self.song1, self.song2), 'The result should be the tuble of songs' def songs_probably_equal_different_song_test(self): """ @@ -138,10 +138,10 @@ class TestLib(TestCase): self.song2.search_lyrics = self.different_lyrics # WHEN: We compare those songs for equality. - result = songs_probably_equal(self.song1, self.song2) + result = songs_probably_equal((self.song1, self.song2)) - # THEN: The result should be False. - assert result is False, 'The result should be False' + # THEN: The result should be None. + assert result is None, 'The result should be None' def remove_typos_beginning_test(self): """ diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 2b5f02483..308881c2e 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -10,6 +10,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 @@ -45,10 +46,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 +59,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 +71,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 +97,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 +118,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 +126,5 @@ 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') diff --git a/tests/functional/openlp_plugins/songs/test_openlyricsimport.py b/tests/functional/openlp_plugins/songs/test_openlyricsimport.py new file mode 100644 index 000000000..93ecafb78 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_openlyricsimport.py @@ -0,0 +1,99 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the OpenLyrics song importer. +""" + +import os +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.songs.lib.openlyricsimport import OpenLyricsImport +from openlp.plugins.songs.lib.songimport import SongImport + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'openlyricssongs')) +SONG_TEST_DATA = { + 'What a friend we have in Jesus.xml': { + 'title': 'What A Friend We Have In Jesus', + 'verses': [ + ('What a friend we have in Jesus, All ours sins and griefs to bear;\n\ + What a privilege to carry, Everything to God in prayer!\n\ + O what peace we often forfeit, O what needless pain we bear;\n\ + All because we do not carry, Everything to God in prayer!', 'v1'), + ('Have we trials and temptations? Is there trouble anywhere?\n\ + We should never be discouraged, Take it to the Lord in prayer.\n\ + Can we find a friend so faithful? Who will all our sorrows share?\n\ + Jesus knows our every weakness; Take it to the Lord in prayer.', 'v2'), + ('Are we weak and heavy laden, Cumbered with a load of care?\n\ + Precious Saviour still our refuge; Take it to the Lord in prayer.\n\ + Do thy friends despise forsake thee? Take it to the Lord in prayer!\n\ + In His arms He’ll take and shield thee; Thou wilt find a solace there.', 'v3') + ] + } +} + + +class TestOpenLyricsImport(TestCase): + """ + Test the functions in the :mod:`openlyricsimport` module. + """ + def create_importer_test(self): + """ + Test creating an instance of the OpenLyrics file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OpenLyricsImport(mocked_manager, filenames=[]) + + # THEN: The importer should be an instance of SongImport + self.assertIsInstance(importer, SongImport) + + def file_import_test(self): + """ + Test the actual import of real song files + """ + # GIVEN: Test files with a mocked out "manager" and a mocked out "import_wizard" + for song_file in SONG_TEST_DATA: + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenLyricsImport(mocked_manager, filenames=[]) + importer.import_wizard = mocked_import_wizard + importer.open_lyrics = MagicMock() + importer.open_lyrics.xml_to_song = MagicMock() + + # WHEN: Importing each file + importer.import_source = [os.path.join(TEST_PATH, song_file)] + importer.do_import() + + # THEN: The xml_to_song() method should have been called + self.assertTrue(importer.open_lyrics.xml_to_song.called) diff --git a/tests/functional/openlp_plugins/songs/test_songbeamerimport.py b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py index e7bd891d3..a69d4a86c 100644 --- a/tests/functional/openlp_plugins/songs/test_songbeamerimport.py +++ b/tests/functional/openlp_plugins/songs/test_songbeamerimport.py @@ -35,6 +35,7 @@ from unittest import TestCase from tests.functional import MagicMock, patch from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport +from openlp.plugins.songs.lib import VerseType TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'songbeamersongs')) @@ -48,7 +49,8 @@ SONG_TEST_DATA = { ('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v') ], 'song_book_name': 'Glaubenslieder I', - 'song_number': "1" + 'song_number': "1", + 'authors': ['Carl Brockhaus', 'Johann Jakob Vetter'] } } @@ -89,8 +91,8 @@ class TestSongBeamerImport(TestCase): # THEN: do_import should return none and the progress bar maximum should not be set. self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list') - self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False, - 'setMaxium on import_wizard.progress_bar should not have been called') + self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False, + 'setMaxium on import_wizard.progress_bar should not have been called') def valid_import_source_test(self): """ @@ -139,17 +141,98 @@ class TestSongBeamerImport(TestCase): add_verse_calls = SONG_TEST_DATA[song_file]['verses'] song_book_name = SONG_TEST_DATA[song_file]['song_book_name'] song_number = SONG_TEST_DATA[song_file]['song_number'] + song_authors = SONG_TEST_DATA[song_file]['authors'] # 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.assertEquals(importer.title, title, 'title for %s should be "%s"' % (song_file, title)) + self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (song_file, title)) for verse_text, verse_tag in add_verse_calls: mocked_add_verse.assert_any_call(verse_text, verse_tag) if song_book_name: - self.assertEquals(importer.song_book_name, song_book_name, 'song_book_name for %s should be "%s"' % - (song_file, song_book_name)) + self.assertEqual(importer.song_book_name, song_book_name, + 'song_book_name for %s should be "%s"' % (song_file, song_book_name)) if song_number: - self.assertEquals(importer.song_number, song_number, 'song_number for %s should be %s' % - (song_file, song_number)) + self.assertEqual(importer.song_number, song_number, + 'song_number for %s should be %s' % (song_file, song_number)) + if song_authors: + for author in importer.authors: + self.assertIn(author, song_authors) mocked_finish.assert_called_with() + + def check_verse_marks_test(self): + """ + Tests different lines to see if a verse mark is detected or not + """ + + # GIVEN: line with unnumbered verse-type + line = 'Refrain' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back true and c as self.current_verse_type + self.assertTrue(result, 'Versemark for should be found, value true') + self.assertEqual(self.current_verse_type, 'c', ' should be interpreted as ') + + # GIVEN: line with unnumbered verse-type and trailing space + line = 'Refrain ' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back true and c as self.current_verse_type + self.assertTrue(result, 'Versemark for should be found, value true') + self.assertEqual(self.current_verse_type, 'c', ' should be interpreted as ') + + # GIVEN: line with numbered verse-type + line = 'Verse 1' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back true and v1 as self.current_verse_type + self.assertTrue(result, 'Versemark for should be found, value true') + self.assertEqual(self.current_verse_type, 'v1', u' should be interpreted as ') + + # GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags) + line = '$$M=special' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back true and o as self.current_verse_type + self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true') + self.assertEqual(self.current_verse_type, 'o', u'<$$M=special> should be interpreted as ') + + # GIVEN: line with song-text with 3 words + line = 'Jesus my saviour' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back false and none as self.current_verse_type + self.assertFalse(result, 'No versemark for should be found, value false') + self.assertIsNone(self.current_verse_type, ' should be interpreted as none versemark') + + # GIVEN: line with song-text with 2 words + line = 'Praise him' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back false and none as self.current_verse_type + self.assertFalse(result, 'No versemark for should be found, value false') + self.assertIsNone(self.current_verse_type, ' should be interpreted as none versemark') + + # GIVEN: line with only a space (could occur, nothing regular) + line = ' ' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back false and none as self.current_verse_type + self.assertFalse(result, 'No versemark for < > should be found, value false') + self.assertIsNone(self.current_verse_type, '< > should be interpreted as none versemark') + + # GIVEN: blank line (could occur, nothing regular) + line = '' + self.current_verse_type = None + # WHEN: line is being checked for verse marks + result = SongBeamerImport.check_verse_marks(self, line) + # THEN: we should get back false and none as self.current_verse_type + self.assertFalse(result, 'No versemark for <> should be found, value false') + self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark') diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 7876558e9..7292bb2b0 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -95,8 +95,8 @@ class TestSongShowPlusImport(TestCase): # THEN: do_import should return none and the progress bar maximum should not be set. self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list') - self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False, - 'setMaximum on import_wizard.progress_bar should not have been called') + self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False, + 'setMaximum on import_wizard.progress_bar should not have been called') def valid_import_source_test(self): """ @@ -116,7 +116,7 @@ class TestSongShowPlusImport(TestCase): # THEN: do_import should return none and the progress bar setMaximum should be called with the length of # import_source. self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is a list ' - 'and stop_import_flag is True') + 'and stop_import_flag is True') mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source)) def to_openlp_verse_tag_test(self): @@ -143,9 +143,9 @@ class TestSongShowPlusImport(TestCase): # THEN: The returned value should should correlate with the input arguments for original_tag, openlp_tag in test_values: - self.assertEquals(importer.to_openlp_verse_tag(original_tag), openlp_tag, - 'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % - (openlp_tag, original_tag)) + self.assertEqual(importer.to_openlp_verse_tag(original_tag), openlp_tag, + 'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % + (openlp_tag, original_tag)) def to_openlp_verse_tag_verse_order_test(self): """ @@ -172,6 +172,6 @@ class TestSongShowPlusImport(TestCase): # THEN: The returned value should should correlate with the input arguments for original_tag, openlp_tag in test_values: - self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag, - 'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % - (openlp_tag, original_tag)) + self.assertEqual(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag, + 'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % + (openlp_tag, original_tag)) diff --git a/tests/functional/openlp_plugins/songs/test_zionworximport.py b/tests/functional/openlp_plugins/songs/test_zionworximport.py new file mode 100644 index 000000000..2edc071c7 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_zionworximport.py @@ -0,0 +1,56 @@ +# -*- 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 # +############################################################################### +""" +This module contains tests for the ZionWorx song importer. +""" + +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.songs.lib.zionworximport import ZionWorxImport +from openlp.plugins.songs.lib.songimport import SongImport + + +class TestZionWorxImport(TestCase): + """ + Test the functions in the :mod:`zionworximport` module. + """ + def create_importer_test(self): + """ + Test creating an instance of the ZionWorx file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = ZionWorxImport(mocked_manager, filenames=[]) + + # THEN: The importer should be an instance of SongImport + self.assertIsInstance(importer, SongImport) diff --git a/tests/functional/test_init.py b/tests/functional/test_init.py new file mode 100644 index 000000000..6fc7d7f70 --- /dev/null +++ b/tests/functional/test_init.py @@ -0,0 +1,70 @@ +# -*- 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.__init__ package. +""" +import os + +from unittest import TestCase +from unittest.mock import MagicMock, patch +from PyQt4 import QtCore + +from openlp.core import OpenLP +from tests.helpers.testmixin import TestMixin + + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'resources')) + + +class TestInit(TestCase, TestMixin): + def setUp(self): + with patch('openlp.core.common.OpenLPMixin.__init__') as constructor: + constructor.return_value = None + self.openlp = OpenLP(list()) + + def tearDown(self): + del self.openlp + + def event_test(self): + """ + Test the reimplemented event method + """ + # GIVEN: A file path and a QEvent. + file_path = os.path.join(TEST_PATH, 'church.jpg') + mocked_file_method = MagicMock(return_value=file_path) + event = QtCore.QEvent(QtCore.QEvent.FileOpen) + event.file = mocked_file_method + + # WHEN: Call the vent method. + result = self.openlp.event(event) + + # THEN: The path should be inserted. + self.assertTrue(result, "The method should have returned True.") + mocked_file_method.assert_called_once_with() + self.assertEqual(self.openlp.args[0], file_path, "The path should be in args.") diff --git a/tests/helpers/songfileimport.py b/tests/helpers/songfileimport.py index cc67770c1..5364c2c3b 100644 --- a/tests/helpers/songfileimport.py +++ b/tests/helpers/songfileimport.py @@ -110,30 +110,30 @@ class SongImportTestHelper(TestCase): # 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.assertEquals(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title)) + self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title)) for author in author_calls: self.mocked_parse_author.assert_any_call(author) if song_copyright: self.mocked_add_copyright.assert_called_with(song_copyright) if ccli_number: - self.assertEquals(importer.ccli_number, ccli_number, 'ccli_number for %s should be %s' % - (source_file_name, ccli_number)) + self.assertEqual(importer.ccli_number, ccli_number, + 'ccli_number for %s should be %s' % (source_file_name, ccli_number)) for verse_text, verse_tag in add_verse_calls: self.mocked_add_verse.assert_any_call(verse_text, verse_tag) if topics: - self.assertEquals(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics)) + self.assertEqual(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics)) if comments: - self.assertEquals(importer.comments, comments, 'comments for %s should be "%s"' % - (source_file_name, comments)) + self.assertEqual(importer.comments, comments, + 'comments for %s should be "%s"' % (source_file_name, comments)) if song_book_name: - self.assertEquals(importer.song_book_name, song_book_name, 'song_book_name for %s should be "%s"' % - (source_file_name, song_book_name)) + self.assertEqual(importer.song_book_name, song_book_name, + 'song_book_name for %s should be "%s"' % (source_file_name, song_book_name)) if song_number: - self.assertEquals(importer.song_number, song_number, 'song_number for %s should be %s' % - (source_file_name, song_number)) + self.assertEqual(importer.song_number, song_number, + 'song_number for %s should be %s' % (source_file_name, song_number)) if verse_order_list: - self.assertEquals(importer.verse_order_list, [], 'verse_order_list for %s should be %s' % - (source_file_name, verse_order_list)) + self.assertEqual(importer.verse_order_list, [], + 'verse_order_list for %s should be %s' % (source_file_name, verse_order_list)) self.mocked_finish.assert_called_with() def _get_data(self, data, key): 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..472bce03f --- /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.") \ No newline at end of file diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py index b085bd1df..e20105ea1 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py @@ -83,7 +83,7 @@ class TestBibleManager(TestCase, TestMixin): # WHEN asking to parse the bible reference results = parse_reference('1 Timothy 1', self.manager.db_cache['tests'], MagicMock(), 54) # THEN a verse array should be returned - self.assertEquals([(54, 1, 1, -1)], results, "The bible verses should matches the expected results") + self.assertEqual([(54, 1, 1, -1)], results, "The bible verses should matches the expected results") def parse_reference_two_test(self): """ @@ -93,7 +93,7 @@ class TestBibleManager(TestCase, TestMixin): # WHEN asking to parse the bible reference results = parse_reference('1 Timothy 1:1-2', self.manager.db_cache['tests'], MagicMock(), 54) # THEN a verse array should be returned - self.assertEquals([(54, 1, 1, 2)], results, "The bible verses should matches the expected results") + self.assertEqual([(54, 1, 1, 2)], results, "The bible verses should matches the expected results") def parse_reference_three_test(self): """ @@ -103,5 +103,5 @@ class TestBibleManager(TestCase, TestMixin): # WHEN asking to parse the bible reference results = parse_reference('1 Timothy 1:1-2:1', self.manager.db_cache['tests'], MagicMock(), 54) # THEN a verse array should be returned - self.assertEquals([(54, 1, 1, -1), (54, 2, 1, 1)], results, "The bible verses should matches the expected " - "results") + self.assertEqual([(54, 1, 1, -1), (54, 2, 1, 1)], results, + "The bible verses should match the expected results") diff --git a/tests/resources/church2.jpg b/tests/resources/church2.jpg new file mode 100644 index 000000000..826180870 Binary files /dev/null and b/tests/resources/church2.jpg differ diff --git a/tests/resources/church3.jpg b/tests/resources/church3.jpg new file mode 100644 index 000000000..826180870 Binary files /dev/null and b/tests/resources/church3.jpg differ diff --git a/tests/resources/church4.jpg b/tests/resources/church4.jpg new file mode 100644 index 000000000..826180870 Binary files /dev/null and b/tests/resources/church4.jpg 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/openlyricssongs/What a friend we have in Jesus.xml b/tests/resources/openlyricssongs/What a friend we have in Jesus.xml new file mode 100644 index 000000000..a111c853c --- /dev/null +++ b/tests/resources/openlyricssongs/What a friend we have in Jesus.xml @@ -0,0 +1,44 @@ + + + + + What A Friend We Have In Jesus + + + Joseph M. Scriven + Charles C. Convers + + Public Domain + 27714 + + Christ: Love/Mercy + Fruit: Peace/Comfort + + + + + + What a friend we have in Jesus, All ours sins and griefs to bear;
+ What a privilege to carry, Everything to God in prayer!
+ O what peace we often forfeit, O what needless pain we bear;
+ All because we do not carry, Everything to God in prayer! +
+
+ + + Have we trials and temptations? Is there trouble anywhere?
+ We should never be discouraged, Take it to the Lord in prayer.
+ Can we find a friend so faithful? Who will all our sorrows share?
+ Jesus knows our every weakness; Take it to the Lord in prayer. +
+
+ + + Are we weak and heavy laden, Cumbered with a load of care?
+ Precious Saviour still our refuge; Take it to the Lord in prayer.
+ Do thy friends despise forsake thee? Take it to the Lord in prayer!
+ In His arms He’ll take and shield thee; Thou wilt find a solace there. +
+
+
+
diff --git a/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng index fbc9aa9fc..c93a143fa 100644 --- a/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng +++ b/tests/resources/songbeamersongs/Lobsinget dem Herrn.sng @@ -1,5 +1,7 @@ #LangCount=1 #Title=GL 1 - Lobsinget dem Herrn +#Author=Carl Brockhaus +#Melody=Johann Jakob Vetter #Editor=SongBeamer 4.20 #Version=3 #Format=F/K// diff --git a/tests/utils/test_bzr_tags.py b/tests/utils/test_bzr_tags.py new file mode 100644 index 000000000..95017e94f --- /dev/null +++ b/tests/utils/test_bzr_tags.py @@ -0,0 +1,75 @@ +# -*- 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 for proper bzr tags. +""" +import os + +from unittest import TestCase + +from subprocess import Popen, PIPE + +TAGS = [ + ['1.9.0', '1'], + ['1.9.1', '775'], + ['1.9.2', '890'], + ['1.9.3', '1063'], + ['1.9.4', '1196'], + ['1.9.5', '1421'], + ['1.9.6', '1657'], + ['1.9.7', '1761'], + ['1.9.8', '1856'], + ['1.9.9', '1917'], + ['1.9.10', '2003'], + ['1.9.11', '2039'], + ['1.9.12', '2063'], + ['2.0', '2118'], + ['2.0.1', '?'], + ['2.0.2', '?'], + ['2.0.3', '?'], + ['2.1.0', '2119'] +] + + +class TestBzrTags(TestCase): + + def bzr_tags_test(self): + """ + Test for proper bzr tags + """ + # GIVEN: A bzr branch + path = os.path.dirname(__file__) + + # WHEN getting the branches tags + bzr = Popen(('bzr', 'tags', '--directory=' + path), stdout=PIPE) + stdout = bzr.communicate()[0] + tags = [line.decode('utf-8').split() for line in stdout.splitlines()] + + # THEN the tags should match the accepted tags + self.assertEqual(TAGS, tags, 'List of tags should match')