CCLI SongSelect integration and code cleanups

bzr-revno: 2330
This commit is contained in:
Raoul Snyman 2014-03-11 20:12:40 +00:00 committed by Tim Bentley
commit 03d6d374a4
19 changed files with 1580 additions and 278 deletions

View File

@ -134,4 +134,4 @@ from .registrymixin import RegistryMixin
from .uistrings import UiStrings
from .settings import Settings
from .applocation import AppLocation
from .historycombobox import HistoryComboBox

View File

@ -0,0 +1,91 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.core.lib.historycombobox` module contains the HistoryComboBox widget
"""
from PyQt4 import QtCore, QtGui
class HistoryComboBox(QtGui.QComboBox):
"""
The :class:`~openlp.core.lib.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` signal
for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit box into
its list.
"""
returnPressed = QtCore.pyqtSignal()
def __init__(self, parent=None):
"""
Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top.
:param parent: The parent widget
"""
super().__init__(parent)
self.setDuplicatesEnabled(False)
self.setEditable(True)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
self.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
def keyPressEvent(self, event):
"""
Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to
the dropdown list.
:param event: The keyboard event
"""
# Handle Enter and Return ourselves
if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return:
# Emit the returnPressed signal
self.returnPressed.emit()
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""
Override the inherited focusOutEvent to save the current text to the dropdown list.
:param event: The focus event
"""
# Save the current text to the dropdown list
if self.currentText() and self.findText(self.currentText()) == -1:
self.insertItem(0, self.currentText())
# Let the parent handle any keypress events
super().focusOutEvent(event)
def getItems(self):
"""
Get all the items from the history
:return: A list of strings
"""
return [self.itemText(i) for i in range(self.count())]

View File

@ -29,6 +29,7 @@
"""
The :mod:`ui` module provides the core user interface for OpenLP
"""
from PyQt4 import QtGui
class HideMode(object):
@ -77,6 +78,29 @@ class DisplayControllerType(object):
Plugin = 2
class SingleColumnTableWidget(QtGui.QTableWidget):
"""
Class to for a single column table widget to use for the verse table widget.
"""
def __init__(self, parent):
"""
Constructor
"""
super(SingleColumnTableWidget, self).__init__(parent)
self.horizontalHeader().setVisible(False)
self.setColumnCount(1)
def resizeEvent(self, event):
"""
Resize the first column together with the widget.
"""
QtGui.QTableWidget.resizeEvent(self, event)
if self.columnCount():
self.setColumnWidth(0, event.size().width())
self.resizeRowsToContents()
from .firsttimeform import FirstTimeForm
from .firsttimelanguageform import FirstTimeLanguageForm
from .themelayoutform import ThemeLayoutForm
@ -101,8 +125,8 @@ from .mediadockmanager import MediaDockManager
from .servicemanager import ServiceManager
from .thememanager import ThemeManager
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager',
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm',
'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm',
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController']
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm',
'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay',
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget']

View File

@ -119,8 +119,10 @@ class Ui_MainWindow(object):
self.recent_files_menu = QtGui.QMenu(self.file_menu)
self.recent_files_menu.setObjectName('recentFilesMenu')
self.file_import_menu = QtGui.QMenu(self.file_menu)
self.file_import_menu.setIcon(build_icon(u':/general/general_import.png'))
self.file_import_menu.setObjectName('file_import_menu')
self.file_export_menu = QtGui.QMenu(self.file_menu)
self.file_export_menu.setIcon(build_icon(u':/general/general_export.png'))
self.file_export_menu.setObjectName('file_export_menu')
# View Menu
self.view_menu = QtGui.QMenu(self.menu_bar)
@ -230,7 +232,7 @@ class Ui_MainWindow(object):
main_window, 'modeDefaultItem', checked=False, category=UiStrings().ViewMode, can_shortcuts=True)
self.mode_setup_item = create_action(main_window, 'modeSetupItem', checked=False, category=UiStrings().ViewMode,
can_shortcuts=True)
self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode,
self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode,
can_shortcuts=True)
self.mode_group = QtGui.QActionGroup(main_window)
self.mode_group.addAction(self.mode_default_item)
@ -239,10 +241,10 @@ class Ui_MainWindow(object):
self.mode_default_item.setChecked(True)
action_list.add_category(UiStrings().Tools, CategoryOrder.standard_menu)
self.tools_add_tool_item = create_action(main_window,
'toolsAddToolItem', icon=':/tools/tools_add.png',
'toolsAddToolItem', icon=':/tools/tools_add.png',
category=UiStrings().Tools, can_shortcuts=True)
self.tools_open_data_folder = create_action(main_window,
'toolsOpenDataFolder', icon=':/general/general_open.png',
'toolsOpenDataFolder', icon=':/general/general_open.png',
category=UiStrings().Tools, can_shortcuts=True)
self.tools_first_time_wizard = create_action(main_window,
'toolsFirstTimeWizard', icon=':/general/general_revert.png',
@ -268,24 +270,24 @@ class Ui_MainWindow(object):
language_item = create_action(main_window, key, checked=qm_list[key] == saved_language)
add_actions(self.language_group, [language_item])
self.settings_shortcuts_item = create_action(main_window, 'settingsShortcutsItem',
icon=':/system/system_configure_shortcuts.png',
icon=':/system/system_configure_shortcuts.png',
category=UiStrings().Settings, can_shortcuts=True)
# Formatting Tags were also known as display tags.
self.formatting_tag_item = create_action(main_window, 'displayTagItem',
icon=':/system/tag_editor.png', category=UiStrings().Settings,
icon=':/system/tag_editor.png', category=UiStrings().Settings,
can_shortcuts=True)
self.settings_configure_item = create_action(main_window, 'settingsConfigureItem',
icon=':/system/system_settings.png', can_shortcuts=True,
icon=':/system/system_settings.png', can_shortcuts=True,
category=UiStrings().Settings)
# Give QT Extra Hint that this is the Preferences Menu Item
self.settings_configure_item.setMenuRole(QtGui.QAction.PreferencesRole)
self.settings_import_item = create_action(main_window, 'settingsImportItem',
self.settings_import_item = create_action(main_window, 'settingsImportItem',
category=UiStrings().Import, can_shortcuts=True)
self.settings_export_item = create_action(main_window, 'settingsExportItem',
self.settings_export_item = create_action(main_window, 'settingsExportItem',
category=UiStrings().Export, can_shortcuts=True)
action_list.add_category(UiStrings().Help, CategoryOrder.standard_menu)
self.about_item = create_action(main_window, 'aboutItem', icon=':/system/system_about.png',
can_shortcuts=True, category=UiStrings().Help,
can_shortcuts=True, category=UiStrings().Help,
triggers=self.on_about_item_clicked)
# Give QT Extra Hint that this is an About Menu Item
self.about_item.setMenuRole(QtGui.QAction.AboutRole)
@ -302,13 +304,13 @@ class Ui_MainWindow(object):
self.web_site_item = create_action(main_window, 'webSiteItem', can_shortcuts=True, category=UiStrings().Help)
# Shortcuts not connected to buttons or menu entries.
self.search_shortcut_action = create_action(main_window,
'searchShortcut', can_shortcuts=True,
'searchShortcut', can_shortcuts=True,
category=translate('OpenLP.MainWindow', 'General'),
triggers=self.on_search_shortcut_triggered)
add_actions(self.file_import_menu, (self.settings_import_item, None, self.import_theme_item,
self.import_language_item))
add_actions(self.file_export_menu, (self.settings_export_item, None, self.export_theme_item,
self.export_language_item))
add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item,
self.import_language_item, None))
add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item,
self.export_language_item, None))
add_actions(self.file_menu, (self.file_new_item, self.file_open_item,
self.file_save_item, self.file_save_as_item, self.recent_files_menu.menuAction(), None,
self.file_import_menu.menuAction(), self.file_export_menu.menuAction(), None,
@ -651,7 +653,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
'Time Wizard?\n\nRe-running this wizard may make changes to your '
'current OpenLP configuration and possibly add songs to your '
'#existing songs list and change your default theme.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No),
QtGui.QMessageBox.No)
if answer == QtGui.QMessageBox.No:

View File

@ -32,6 +32,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box, create_button
from openlp.core.ui import SingleColumnTableWidget
from openlp.plugins.songs.lib.ui import SongStrings
@ -346,25 +347,3 @@ def create_combo_box(parent, name):
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name)
return combo_box
class SingleColumnTableWidget(QtGui.QTableWidget):
"""
Class to for a single column table widget to use for the verse table widget.
"""
def __init__(self, parent):
"""
Constructor
"""
super(SingleColumnTableWidget, self).__init__(parent)
self.horizontalHeader().setVisible(False)
self.setColumnCount(1)
def resizeEvent(self, event):
"""
Resize the first column together with the widget.
"""
QtGui.QTableWidget.resizeEvent(self, event)
if self.columnCount():
self.setColumnWidth(0, event.size().width())
self.resizeRowsToContents()

View File

@ -0,0 +1,252 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.plugins.songs.forms.songselectdialog` module contains the user interface code for the dialog
"""
from PyQt4 import QtCore, QtGui
from openlp.core.common import HistoryComboBox
from openlp.core.lib import translate, build_icon
from openlp.core.ui import SingleColumnTableWidget
class Ui_SongSelectDialog(object):
"""
The actual Qt components that make up the dialog.
"""
def setup_ui(self, songselect_dialog):
songselect_dialog.setObjectName('songselect_dialog')
songselect_dialog.resize(616, 378)
self.songselect_layout = QtGui.QVBoxLayout(songselect_dialog)
self.songselect_layout.setSpacing(0)
self.songselect_layout.setMargin(0)
self.songselect_layout.setObjectName('songselect_layout')
self.stacked_widget = QtGui.QStackedWidget(songselect_dialog)
self.stacked_widget.setObjectName('stacked_widget')
self.login_page = QtGui.QWidget()
self.login_page.setObjectName('login_page')
self.login_layout = QtGui.QFormLayout(self.login_page)
self.login_layout.setContentsMargins(120, 100, 120, 100)
self.login_layout.setSpacing(8)
self.login_layout.setObjectName('login_layout')
self.notice_layout = QtGui.QHBoxLayout()
self.notice_layout.setObjectName('notice_layout')
self.notice_label = QtGui.QLabel(self.login_page)
self.notice_label.setWordWrap(True)
self.notice_label.setObjectName('notice_label')
self.notice_layout.addWidget(self.notice_label)
self.login_layout.setLayout(0, QtGui.QFormLayout.SpanningRole, self.notice_layout)
self.username_label = QtGui.QLabel(self.login_page)
self.username_label.setObjectName('usernameLabel')
self.login_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.username_label)
self.username_edit = QtGui.QLineEdit(self.login_page)
self.username_edit.setObjectName('usernameEdit')
self.login_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.username_edit)
self.password_label = QtGui.QLabel(self.login_page)
self.password_label.setObjectName('passwordLabel')
self.login_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.password_label)
self.password_edit = QtGui.QLineEdit(self.login_page)
self.password_edit.setEchoMode(QtGui.QLineEdit.Password)
self.password_edit.setObjectName('passwordEdit')
self.login_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.password_edit)
self.save_password_checkbox = QtGui.QCheckBox(self.login_page)
self.save_password_checkbox.setTristate(False)
self.save_password_checkbox.setObjectName('save_password_checkbox')
self.login_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.save_password_checkbox)
self.login_button_layout = QtGui.QHBoxLayout()
self.login_button_layout.setSpacing(8)
self.login_button_layout.setContentsMargins(0, -1, -1, -1)
self.login_button_layout.setObjectName('login_button_layout')
self.login_spacer = QtGui.QWidget(self.login_page)
self.login_spacer.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.login_spacer.setObjectName('login_spacer')
self.login_button_layout.addWidget(self.login_spacer)
self.login_progress_bar = QtGui.QProgressBar(self.login_page)
self.login_progress_bar.setMinimum(0)
self.login_progress_bar.setMaximum(3)
self.login_progress_bar.setValue(0)
self.login_progress_bar.setMinimumWidth(200)
self.login_progress_bar.setVisible(False)
self.login_button_layout.addWidget(self.login_progress_bar)
self.login_button = QtGui.QPushButton(self.login_page)
self.login_button.setIcon(build_icon(':/songs/song_author_edit.png'))
self.login_button.setObjectName('login_button')
self.login_button_layout.addWidget(self.login_button)
self.login_layout.setLayout(4, QtGui.QFormLayout.SpanningRole, self.login_button_layout)
self.stacked_widget.addWidget(self.login_page)
self.search_page = QtGui.QWidget()
self.search_page.setObjectName('search_page')
self.search_layout = QtGui.QVBoxLayout(self.search_page)
self.search_layout.setSpacing(8)
self.search_layout.setMargin(8)
self.search_layout.setObjectName('search_layout')
self.search_input_layout = QtGui.QHBoxLayout()
self.search_input_layout.setSpacing(8)
self.search_input_layout.setObjectName('search_input_layout')
self.search_label = QtGui.QLabel(self.search_page)
self.search_label.setObjectName('search_label')
self.search_input_layout.addWidget(self.search_label)
self.search_combobox = HistoryComboBox(self.search_page)
self.search_combobox.setObjectName('search_combobox')
self.search_input_layout.addWidget(self.search_combobox)
self.search_button = QtGui.QPushButton(self.search_page)
self.search_button.setIcon(build_icon(':/general/general_find.png'))
self.search_button.setObjectName('search_button')
self.search_input_layout.addWidget(self.search_button)
self.search_layout.addLayout(self.search_input_layout)
self.search_progress_bar = QtGui.QProgressBar(self.search_page)
self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(3)
self.search_progress_bar.setValue(0)
self.search_progress_bar.setVisible(False)
self.search_layout.addWidget(self.search_progress_bar)
self.search_results_widget = QtGui.QListWidget(self.search_page)
self.search_results_widget.setProperty("showDropIndicator", False)
self.search_results_widget.setAlternatingRowColors(True)
self.search_results_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.search_results_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.search_results_widget.setObjectName('search_results_widget')
self.search_layout.addWidget(self.search_results_widget)
self.result_count_label = QtGui.QLabel(self.search_page)
self.result_count_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
self.result_count_label.setObjectName('result_count_label')
self.search_layout.addWidget(self.result_count_label)
self.view_layout = QtGui.QHBoxLayout()
self.view_layout.setSpacing(8)
self.view_layout.setObjectName('view_layout')
self.logout_button = QtGui.QPushButton(self.search_page)
self.logout_button.setIcon(build_icon(':/songs/song_author_edit.png'))
self.view_layout.addWidget(self.logout_button)
self.view_spacer = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.view_layout.addItem(self.view_spacer)
self.view_button = QtGui.QPushButton(self.search_page)
self.view_button.setIcon(build_icon(':/songs/song_search_all.png'))
self.view_button.setObjectName('view_button')
self.view_layout.addWidget(self.view_button)
self.search_layout.addLayout(self.view_layout)
self.stacked_widget.addWidget(self.search_page)
self.song_page = QtGui.QWidget()
self.song_page.setObjectName('song_page')
self.song_layout = QtGui.QGridLayout(self.song_page)
self.song_layout.setMargin(8)
self.song_layout.setSpacing(8)
self.song_layout.setObjectName('song_layout')
self.title_label = QtGui.QLabel(self.song_page)
self.title_label.setObjectName('title_label')
self.song_layout.addWidget(self.title_label, 0, 0, 1, 1)
self.title_edit = QtGui.QLineEdit(self.song_page)
self.title_edit.setReadOnly(True)
self.title_edit.setObjectName('title_edit')
self.song_layout.addWidget(self.title_edit, 0, 1, 1, 1)
self.authors_label = QtGui.QLabel(self.song_page)
self.authors_label.setObjectName('authors_label')
self.song_layout.addWidget(self.authors_label, 0, 2, 1, 1)
self.author_list_widget = QtGui.QListWidget(self.song_page)
self.author_list_widget.setObjectName('author_list_widget')
self.song_layout.addWidget(self.author_list_widget, 0, 3, 3, 1)
self.copyright_label = QtGui.QLabel(self.song_page)
self.copyright_label.setObjectName('copyright_label')
self.song_layout.addWidget(self.copyright_label, 1, 0, 1, 1)
self.copyright_edit = QtGui.QLineEdit(self.song_page)
self.copyright_edit.setReadOnly(True)
self.copyright_edit.setObjectName('copyright_edit')
self.song_layout.addWidget(self.copyright_edit, 1, 1, 1, 1)
self.ccli_label = QtGui.QLabel(self.song_page)
self.ccli_label.setObjectName('ccli_label')
self.song_layout.addWidget(self.ccli_label, 2, 0, 1, 1)
self.ccli_edit = QtGui.QLineEdit(self.song_page)
self.ccli_edit.setReadOnly(True)
self.ccli_edit.setObjectName('ccli_edit')
self.song_layout.addWidget(self.ccli_edit, 2, 1, 1, 1)
self.lyrics_label = QtGui.QLabel(self.song_page)
self.lyrics_label.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.lyrics_label.setObjectName('lyrics_label')
self.song_layout.addWidget(self.lyrics_label, 3, 0, 1, 1)
self.lyrics_table_widget = SingleColumnTableWidget(self.song_page)
self.lyrics_table_widget.setObjectName('lyrics_table_widget')
self.lyrics_table_widget.setRowCount(0)
self.song_layout.addWidget(self.lyrics_table_widget, 3, 1, 1, 3)
self.song_progress_bar = QtGui.QProgressBar(self.song_page)
self.song_progress_bar.setMinimum(0)
self.song_progress_bar.setMaximum(3)
self.song_progress_bar.setValue(0)
self.song_progress_bar.setVisible(False)
self.song_layout.addWidget(self.song_progress_bar, 4, 0, 1, 4)
self.import_layout = QtGui.QHBoxLayout()
self.import_layout.setObjectName('import_layout')
self.back_button = QtGui.QPushButton(self.song_page)
self.back_button.setIcon(build_icon(':/general/general_back.png'))
self.back_button.setObjectName('back_button')
self.import_layout.addWidget(self.back_button)
self.import_spacer = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.import_layout.addItem(self.import_spacer)
self.import_button = QtGui.QPushButton(self.song_page)
self.import_button.setIcon(build_icon(':/general/general_import.png'))
self.import_button.setObjectName('import_button')
self.import_layout.addWidget(self.import_button)
self.song_layout.addLayout(self.import_layout, 5, 0, 1, 5)
self.stacked_widget.addWidget(self.song_page)
self.songselect_layout.addWidget(self.stacked_widget)
self.username_label.setBuddy(self.username_edit)
self.password_label.setBuddy(self.password_edit)
self.title_label.setBuddy(self.title_edit)
self.authors_label.setBuddy(self.author_list_widget)
self.copyright_label.setBuddy(self.copyright_edit)
self.ccli_label.setBuddy(self.ccli_edit)
self.lyrics_label.setBuddy(self.lyrics_table_widget)
self.retranslate_ui(songselect_dialog)
self.stacked_widget.setCurrentIndex(0)
def retranslate_ui(self, songselect_dialog):
"""
Translate the GUI.
"""
songselect_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'CCLI SongSelect Importer'))
self.notice_label.setText(
translate('SongsPlugin.SongSelectForm', '<strong>Note:</strong> '
'An Internet connection is required in order to import songs from CCLI SongSelect.')
)
self.username_label.setText(translate('SongsPlugin.SongSelectForm', 'Username:'))
self.password_label.setText(translate('SongsPlugin.SongSelectForm', 'Password:'))
self.save_password_checkbox.setText(translate('SongsPlugin.SongSelectForm', 'Save username and password'))
self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login'))
self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:'))
self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search'))
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % 0)
self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout'))
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View'))
self.title_label.setText(translate('SongsPlugin.SongSelectForm', 'Title:'))
self.authors_label.setText(translate('SongsPlugin.SongSelectForm', 'Author(s):'))
self.copyright_label.setText(translate('SongsPlugin.SongSelectForm', 'Copyright:'))
self.ccli_label.setText(translate('SongsPlugin.SongSelectForm', 'CCLI Number:'))
self.lyrics_label.setText(translate('SongsPlugin.SongSelectForm', 'Lyrics:'))
self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back'))
self.import_button.setText(translate('SongsPlugin.SongSelectForm', 'Import'))

View File

@ -0,0 +1,387 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer
"""
import logging
import os
from time import sleep
from PyQt4 import QtCore, QtGui
from openlp.core import Settings
from openlp.core.common import Registry
from openlp.core.lib import translate
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
from openlp.plugins.songs.lib.songselect import SongSelectImport
log = logging.getLogger(__name__)
class SearchWorker(QtCore.QObject):
"""
Run the actual SongSelect search, and notify the GUI when we find each song.
"""
show_info = QtCore.pyqtSignal(str, str)
found_song = QtCore.pyqtSignal(dict)
finished = QtCore.pyqtSignal()
quit = QtCore.pyqtSignal()
def __init__(self, importer, search_text):
super().__init__()
self.importer = importer
self.search_text = search_text
def start(self):
"""
Run a search and then parse the results page of the search.
"""
songs = self.importer.search(self.search_text, 1000, self._found_song_callback)
if len(songs) >= 1000:
self.show_info.emit(
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
'been stopped. Please refine your search to fetch better '
'results.'))
self.finished.emit()
self.quit.emit()
def _found_song_callback(self, song):
"""
A callback used by the paginate function to notify watching processes when it finds a song.
:param song: The song that was found
"""
self.found_song.emit(song)
class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
"""
The :class:`SongSelectForm` class is the SongSelect dialog.
"""
def __init__(self, parent=None, plugin=None, db_manager=None):
QtGui.QDialog.__init__(self, parent)
self.setup_ui(self)
self.thread = None
self.worker = None
self.song_count = 0
self.song = None
self.plugin = plugin
self.song_select_importer = SongSelectImport(db_manager)
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
self.login_button.clicked.connect(self.on_login_button_clicked)
self.search_button.clicked.connect(self.on_search_button_clicked)
self.search_combobox.returnPressed.connect(self.on_search_button_clicked)
self.logout_button.clicked.connect(self.done)
self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked)
self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed)
self.view_button.clicked.connect(self.on_view_button_clicked)
self.back_button.clicked.connect(self.on_back_button_clicked)
self.import_button.clicked.connect(self.on_import_button_clicked)
def exec_(self):
"""
Execute the dialog. This method sets everything back to its initial
values.
"""
self.stacked_widget.setCurrentIndex(0)
self.username_edit.setEnabled(True)
self.password_edit.setEnabled(True)
self.save_password_checkbox.setEnabled(True)
self.search_combobox.clearEditText()
self.search_combobox.clear()
self.search_results_widget.clear()
self.view_button.setEnabled(False)
if Settings().contains(self.plugin.settings_section + '/songselect password'):
self.username_edit.setText(Settings().value(self.plugin.settings_section + '/songselect username'))
self.password_edit.setText(Settings().value(self.plugin.settings_section + '/songselect password'))
self.save_password_checkbox.setChecked(True)
if Settings().contains(self.plugin.settings_section + '/songselect searches'):
self.search_combobox.addItems(
Settings().value(self.plugin.settings_section + '/songselect searches').split('|'))
self.username_edit.setFocus()
return QtGui.QDialog.exec_(self)
def done(self, r):
"""
Log out of SongSelect.
:param r: The result of the dialog.
"""
log.debug('Closing SongSelectForm')
if self.stacked_widget.currentIndex() > 0:
progress_dialog = QtGui.QProgressDialog(
translate('SongsPlugin.SongSelectForm', 'Logging out...'), '', 0, 2, self)
progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
progress_dialog.setCancelButton(None)
progress_dialog.setValue(1)
progress_dialog.show()
progress_dialog.setFocus()
self.application.process_events()
sleep(0.5)
self.application.process_events()
self.song_select_importer.logout()
self.application.process_events()
progress_dialog.setValue(2)
return QtGui.QDialog.done(self, r)
def _update_login_progress(self):
self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
self.application.process_events()
def _update_song_progress(self):
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
self.application.process_events()
def _view_song(self, current_item):
if not current_item:
return
else:
current_item = current_item.data(QtCore.Qt.UserRole)
self.song_progress_bar.setVisible(True)
self.import_button.setEnabled(False)
self.back_button.setEnabled(False)
self.title_edit.setText('')
self.title_edit.setEnabled(False)
self.copyright_edit.setText('')
self.copyright_edit.setEnabled(False)
self.ccli_edit.setText('')
self.ccli_edit.setEnabled(False)
self.author_list_widget.clear()
self.author_list_widget.setEnabled(False)
self.lyrics_table_widget.clear()
self.lyrics_table_widget.setRowCount(0)
self.lyrics_table_widget.setEnabled(False)
self.stacked_widget.setCurrentIndex(2)
song = {}
for key, value in current_item.items():
song[key] = value
self.song_progress_bar.setValue(0)
self.application.process_events()
# Get the full song
song = self.song_select_importer.get_song(song, self._update_song_progress)
# Update the UI
self.title_edit.setText(song['title'])
self.copyright_edit.setText(song['copyright'])
self.ccli_edit.setText(song['ccli_number'])
for author in song['authors']:
QtGui.QListWidgetItem(author, self.author_list_widget)
for counter, verse in enumerate(song['verses']):
self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
item = QtGui.QTableWidgetItem(verse['lyrics'])
item.setData(QtCore.Qt.UserRole, verse['label'])
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
self.lyrics_table_widget.setItem(counter, 0, item)
self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in song['verses']])
self.lyrics_table_widget.resizeRowsToContents()
self.title_edit.setEnabled(True)
self.copyright_edit.setEnabled(True)
self.ccli_edit.setEnabled(True)
self.author_list_widget.setEnabled(True)
self.lyrics_table_widget.setEnabled(True)
self.lyrics_table_widget.repaint()
self.import_button.setEnabled(True)
self.back_button.setEnabled(True)
self.song_progress_bar.setVisible(False)
self.song_progress_bar.setValue(0)
self.song = song
self.application.process_events()
def on_save_password_checkbox_toggled(self, checked):
"""
Show a warning dialog when the user toggles the save checkbox on or off.
:param checked: If the combobox is checked or not
"""
if checked and self.login_page.isVisible():
answer = QtGui.QMessageBox.question(
self, translate('SongsPlugin.SongSelectForm', 'Save Username and Password'),
translate('SongsPlugin.SongSelectForm', 'WARNING: Saving your username and password is INSECURE, your '
'password is stored in PLAIN TEXT. Click Yes to save your '
'password or No to cancel this.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), QtGui.QMessageBox.No)
if answer == QtGui.QMessageBox.No:
self.save_password_checkbox.setChecked(False)
def on_login_button_clicked(self):
"""
Log the user in to SongSelect.
"""
self.username_edit.setEnabled(False)
self.password_edit.setEnabled(False)
self.save_password_checkbox.setEnabled(False)
self.login_button.setEnabled(False)
self.login_spacer.setVisible(False)
self.login_progress_bar.setValue(0)
self.login_progress_bar.setVisible(True)
self.application.process_events()
# Log the user in
if not self.song_select_importer.login(
self.username_edit.text(), self.password_edit.text(), self._update_login_progress):
QtGui.QMessageBox.critical(
self,
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
translate('SongsPlugin.SongSelectForm',
'There was a problem logging in, perhaps your username or password is incorrect?')
)
else:
if self.save_password_checkbox.isChecked():
Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text())
Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text())
else:
Settings().remove(self.plugin.settings_section + '/songselect username')
Settings().remove(self.plugin.settings_section + '/songselect password')
self.stacked_widget.setCurrentIndex(1)
self.login_progress_bar.setVisible(False)
self.login_progress_bar.setValue(0)
self.login_spacer.setVisible(True)
self.login_button.setEnabled(True)
self.search_combobox.setFocus()
self.application.process_events()
def on_search_button_clicked(self):
"""
Run a search on SongSelect.
"""
# Set up UI components
self.view_button.setEnabled(False)
self.search_button.setEnabled(False)
self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(0)
self.search_progress_bar.setValue(0)
self.search_progress_bar.setVisible(True)
self.search_results_widget.clear()
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
self.application.process_events()
self.song_count = 0
search_history = self.search_combobox.getItems()
Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history))
# Create thread and run search
self.thread = QtCore.QThread()
self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.start)
self.worker.show_info.connect(self.on_search_show_info)
self.worker.found_song.connect(self.on_search_found_song)
self.worker.finished.connect(self.on_search_finished)
self.worker.quit.connect(self.thread.quit)
self.worker.quit.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
def on_search_show_info(self, title, message):
"""
Show an informational message from the search thread
:param title:
:param message:
"""
QtGui.QMessageBox.information(self, title, message)
def on_search_found_song(self, song):
"""
Add a song to the list when one is found.
:param song:
"""
self.song_count += 1
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
song_item = QtGui.QListWidgetItem(item_title, self.search_results_widget)
song_item.setData(QtCore.Qt.UserRole, song)
def on_search_finished(self):
"""
Slot which is called when the search is completed.
:param songs:
"""
self.application.process_events()
self.search_progress_bar.setVisible(False)
self.search_button.setEnabled(True)
self.application.process_events()
def on_search_results_widget_selection_changed(self):
"""
Enable or disable the view button when the selection changes.
"""
self.view_button.setEnabled(len(self.search_results_widget.selectedItems()) > 0)
def on_view_button_clicked(self):
"""
View a song from SongSelect.
"""
self._view_song(self.search_results_widget.currentItem())
def on_search_results_widget_double_clicked(self, current_item):
"""
View a song from SongSelect
:param current_item:
"""
self._view_song(current_item)
def on_back_button_clicked(self):
"""
Go back to the search page.
"""
self.stacked_widget.setCurrentIndex(1)
self.search_combobox.setFocus()
def on_import_button_clicked(self):
"""
Import a song from SongSelect.
"""
self.song_select_importer.save_song(self.song)
question_dialog = QtGui.QMessageBox()
question_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'Song Imported'))
question_dialog.setText(translate('SongsPlugin.SongSelectForm', 'Your song has been imported, would you like '
'to exit now, or import more songs?'))
question_dialog.addButton(QtGui.QPushButton(translate('SongsPlugin.SongSelectForm', 'Import More Songs')),
QtGui.QMessageBox.YesRole)
question_dialog.addButton(QtGui.QPushButton(translate('SongsPlugin.SongSelectForm', 'Exit Now')),
QtGui.QMessageBox.NoRole)
if question_dialog.exec_() == QtGui.QMessageBox.Yes:
self.on_back_button_clicked()
else:
self.application.process_events()
self.done(QtGui.QDialog.Accepted)
@property
def application(self):
"""
Adds the openlp to the class dynamically.
Windows needs to access the application in a dynamic manner.
"""
if os.name == 'nt':
return Registry().get('application')
else:
if not hasattr(self, '_application'):
self._application = Registry().get('application')
return self._application

View File

@ -53,64 +53,41 @@ APOSTROPHE = re.compile('[\'`ʻ]', re.UNICODE)
# \# - where # is a single non-alpha character, representing a special symbol
# { or } - marking the beginning/end of a group
# a run of characters without any \ { } or end-of-line
PATTERN = re.compile(r"(\\\*)?\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z*])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
PATTERN = re.compile(
r"(\\\*)?\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z*])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
# RTF control words which specify a "destination" to be ignored.
DESTINATIONS = frozenset((
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor',
'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref', 'atntime',
'atrfend', 'atrfstart', 'author', 'background', 'bkmkend',
'bkmkstart', 'blipuid', 'buptim', 'category',
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim',
'datafield', 'datastore', 'defchp', 'defpap', 'do', 'doccomm',
'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname',
'falt', 'fchars', 'ffdeftext', 'ffentrymcr', 'ffexitmcr',
'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext',
'file', 'filetbl', 'fldinst', 'fldtype', 'fname',
'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr',
'footnote', 'formfield', 'ftncn', 'ftnsep', 'ftnsepc', 'g',
'generator', 'gridtbl', 'header', 'headerf', 'headerl',
'headerr', 'hl', 'hlfr', 'hlinkbase', 'hlloc', 'hlsrc', 'hsv',
'htmltag', 'info', 'keycode', 'keywords', 'latentstyles',
'lchars', 'levelnumbers', 'leveltext', 'lfolevel', 'linkval',
'list', 'listlevel', 'listname', 'listoverride',
'listoverridetable', 'listpicture', 'liststylename', 'listtable',
'listtext', 'lsdlockedexcept', 'macc', 'maccPr', 'mailmerge',
'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr',
'mbaseJc', 'mbegChr', 'mborderBox', 'mborderBoxPr', 'mbox',
'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide',
'mden', 'mdiff', 'mdPr', 'me', 'mendChr', 'meqArr', 'meqArrPr',
'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr',
'mgroupChrPr', 'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight',
'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow',
'mlimlowPr', 'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname',
'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs',
'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname',
'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag',
'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr', 'mnoBreak',
'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr',
'mopEmu', 'mphant', 'mphantPr', 'mplcHide', 'mpos', 'mr',
'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre',
'msPrePr', 'msSub', 'msSubPr', 'msSubSup', 'msSubSupPr', 'msSup',
'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV',
'msub', 'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype',
'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile',
'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops',
'oldsprops', 'oldtprops', 'oleclsid', 'operator', 'panose',
'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict',
'pn', 'pnseclvl', 'pntext', 'pntxta', 'pntxtb', 'printim',
'private', 'propname', 'protend', 'protstart', 'protusertbl',
'pxe', 'result', 'revtbl', 'revtim', 'rsidtbl', 'rxe', 'shp',
'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp',
'staticval', 'stylesheet', 'subject', 'sv', 'svb', 'tc',
'template', 'themedata', 'title', 'txe', 'ud', 'upr',
'userprops', 'wgrffmtfilter', 'windowcaption', 'writereservation',
'writereservhash', 'xe', 'xform', 'xmlattrname', 'xmlattrvalue',
'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'))
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor', 'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref',
'atntime', 'atrfend', 'atrfstart', 'author', 'background', 'bkmkend', 'bkmkstart', 'blipuid', 'buptim', 'category',
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim', 'datafield', 'datastore', 'defchp', 'defpap',
'do', 'doccomm', 'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname', 'falt', 'fchars', 'ffdeftext',
'ffentrymcr', 'ffexitmcr', 'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext', 'file', 'filetbl', 'fldinst',
'fldtype', 'fname', 'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr', 'footnote', 'formfield',
'ftncn', 'ftnsep', 'ftnsepc', 'g', 'generator', 'gridtbl', 'header', 'headerf', 'headerl', 'headerr', 'hl', 'hlfr',
'hlinkbase', 'hlloc', 'hlsrc', 'hsv', 'htmltag', 'info', 'keycode', 'keywords', 'latentstyles', 'lchars',
'levelnumbers', 'leveltext', 'lfolevel', 'linkval', 'list', 'listlevel', 'listname', 'listoverride',
'listoverridetable', 'listpicture', 'liststylename', 'listtable', 'listtext', 'lsdlockedexcept', 'macc', 'maccPr',
'mailmerge', 'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr', 'mbaseJc', 'mbegChr', 'mborderBox',
'mborderBoxPr', 'mbox', 'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide', 'mden', 'mdiff', 'mdPr',
'me', 'mendChr', 'meqArr', 'meqArrPr', 'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr', 'mgroupChrPr',
'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight', 'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow', 'mlimlowPr',
'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname', 'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs', 'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname', 'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag', 'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr',
'mnoBreak', 'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr', 'mopEmu', 'mphant', 'mphantPr', 'mplcHide',
'mpos', 'mr', 'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre', 'msPrePr', 'msSub', 'msSubPr',
'msSubSup', 'msSubSupPr', 'msSup', 'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV', 'msub',
'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype', 'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile', 'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops', 'oldsprops', 'oldtprops', 'oleclsid', 'operator',
'panose', 'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict', 'pn', 'pnseclvl', 'pntext', 'pntxta',
'pntxtb', 'printim', 'private', 'propname', 'protend', 'protstart', 'protusertbl', 'pxe', 'result', 'revtbl',
'revtim', 'rsidtbl', 'rxe', 'shp', 'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp', 'staticval',
'stylesheet', 'subject', 'sv', 'svb', 'tc', 'template', 'themedata', 'title', 'txe', 'ud', 'upr', 'userprops',
'wgrffmtfilter', 'windowcaption', 'writereservation', 'writereservhash', 'xe', 'xform', 'xmlattrname',
'xmlattrvalue', 'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'
))
# Translation of some special characters.
SPECIAL_CHARS = {
'\n': '\n',
@ -142,7 +119,8 @@ SPECIAL_CHARS = {
'ltrmark': '\u200E',
'rtlmark': '\u200F',
'zwj': '\u200D',
'zwnj': '\u200C'}
'zwnj': '\u200C'
}
CHARSET_MAPPING = {
'0': 'cp1252',
'128': 'cp932',
@ -156,7 +134,8 @@ CHARSET_MAPPING = {
'186': 'cp1257',
'204': 'cp1251',
'222': 'cp874',
'238': 'cp1250'}
'238': 'cp1250'
}
class VerseType(object):
@ -171,14 +150,7 @@ class VerseType(object):
Ending = 5
Other = 6
names = [
'Verse',
'Chorus',
'Bridge',
'Pre-Chorus',
'Intro',
'Ending',
'Other']
names = ['Verse', 'Chorus', 'Bridge', 'Pre-Chorus', 'Intro', 'Ending', 'Other']
tags = [name[0].lower() for name in names]
translated_names = [
@ -197,11 +169,9 @@ class VerseType(object):
"""
Return the translated UPPERCASE tag for a given tag, used to show translated verse tags in UI
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: A translated UPPERCASE tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -217,11 +187,9 @@ class VerseType(object):
"""
Return the translated name for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: Translated name for the given tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -237,11 +205,9 @@ class VerseType(object):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: A VerseType of the tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -257,11 +223,9 @@ class VerseType(object):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: The VerseType of a translated tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.translated_tags):
@ -277,11 +241,9 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_name: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: The VerseType determined from the string
"""
verse_name = verse_name.lower()
for num, name in enumerate(VerseType.names):
@ -294,8 +256,8 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
:param verse_name: The string to return a VerseType for
:return: A VerseType
"""
verse_name = verse_name.lower()
for num, translation in enumerate(VerseType.translated_names):
@ -307,11 +269,9 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_name: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:return: A VerseType
"""
if len(verse_name) > 1:
verse_index = VerseType.from_translated_string(verse_name)
@ -331,22 +291,21 @@ def retrieve_windows_encoding(recommendation=None):
Determines which encoding to use on an information source. The process uses both automated detection, which is
passed to this method as a recommendation, and user confirmation to return an encoding.
``recommendation``
A recommended encoding discovered programmatically for the user to confirm.
:param recommendation: A recommended encoding discovered programmatically for the user to confirm.
:return: A list of recommended encodings, or None
"""
# map chardet result to compatible windows standard code page
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874',
'SHIFT_JIS': 'cp932', 'GB2312': 'cp936', 'HZ-GB-2312': 'cp936',
'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
'windows-1250': 'cp1250', 'windows-1251': 'cp1251',
'windows-1252': 'cp1252', 'ISO-8859-7': 'cp1253',
'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
'windows-1255': 'cp1255'}
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874', 'SHIFT_JIS': 'cp932', 'GB2312': 'cp936',
'HZ-GB-2312': 'cp936', 'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
'windows-1250': 'cp1250', 'windows-1251': 'cp1251', 'windows-1252': 'cp1252',
'ISO-8859-7': 'cp1253', 'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
'windows-1255': 'cp1255'}
if recommendation in codepage_mapping:
recommendation = codepage_mapping[recommendation]
# Show dialog for encoding selection
encodings = [('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
encodings = [
('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
('cp1257', translate('SongsPlugin', 'Baltic (CP-1257)')),
('cp1250', translate('SongsPlugin', 'Central European (CP-1250)')),
('cp1251', translate('SongsPlugin', 'Cyrillic (CP-1251)')),
@ -359,7 +318,8 @@ def retrieve_windows_encoding(recommendation=None):
('cp950', translate('SongsPlugin', 'Traditional Chinese (CP-950)')),
('cp1254', translate('SongsPlugin', 'Turkish (CP-1254)')),
('cp1258', translate('SongsPlugin', 'Vietnam (CP-1258)')),
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))]
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))
]
recommended_index = -1
if recommendation:
for index in range(len(encodings)):
@ -367,17 +327,20 @@ def retrieve_windows_encoding(recommendation=None):
recommended_index = index
break
if recommended_index > -1:
choice = QtGui.QInputDialog.getItem(None,
choice = QtGui.QInputDialog.getItem(
None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'The codepage setting is responsible\n'
'for the correct character representation.\nUsually you are fine with the preselected choice.'),
'for the correct character representation.\n'
'Usually you are fine with the preselected choice.'),
[pair[1] for pair in encodings], recommended_index, False)
else:
choice = QtGui.QInputDialog.getItem(None,
choice = QtGui.QInputDialog.getItem(
None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'Please choose the character encoding.\n'
'The encoding is responsible for the correct character representation.'),
[pair[1] for pair in encodings], 0, False)
'The encoding is responsible for the correct character representation.'),
[pair[1] for pair in encodings], 0, False)
if not choice[1]:
return None
return next(filter(lambda item: item[1] == choice[0], encodings))[0]
@ -386,6 +349,9 @@ def retrieve_windows_encoding(recommendation=None):
def clean_string(string):
"""
Strips punctuation from the passed string to assist searching.
:param string: The string to clean
:return: A clean string
"""
return WHITESPACE.sub(' ', APOSTROPHE.sub('', string)).lower()
@ -393,6 +359,9 @@ def clean_string(string):
def clean_title(title):
"""
Cleans the song title by removing Unicode control chars groups C0 & C1, as well as any trailing spaces.
:param title: The song title to clean
:return: A clean title
"""
return CONTROL_CHARS.sub('', title).rstrip()
@ -402,11 +371,8 @@ def clean_song(manager, song):
Cleans the search title, rebuilds the search lyrics, adds a default author if the song does not have one and other
clean ups. This should always called when a new song is added or changed.
``manager``
The song's manager.
``song``
The song object.
:param manager: The song database manager object.
:param song: The song object.
"""
from .xml import SongXML
@ -419,55 +385,10 @@ def clean_song(manager, song):
else:
song.alternate_title = ''
song.search_title = clean_string(song.title) + '@' + clean_string(song.alternate_title)
# Only do this, if we the song is a 1.9.4 song (or older).
if song.lyrics.find('<lyrics language="en">') != -1:
# Remove the old "language" attribute from lyrics tag (prior to 1.9.5). This is not very important, but this
# keeps the database clean. This can be removed when everybody has cleaned his songs.
song.lyrics = song.lyrics.replace('<lyrics language="en">', '<lyrics>')
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1])
for verse in verses])
# We need a new and clean SongXML instance.
sxml = SongXML()
# Rebuild the song's verses, to remove any wrong verse names (for example translated ones), which might have
# been added prior to 1.9.5.
# List for later comparison.
compare_order = []
for verse in verses:
verse_type = VerseType.tags[VerseType.from_loose_input(verse[0]['type'])]
sxml.add_verse_to_lyrics(
verse_type,
verse[0]['label'],
verse[1],
verse[0].get('lang')
)
compare_order.append(('%s%s' % (verse_type, verse[0]['label'])).upper())
if verse[0]['label'] == '1':
compare_order.append(verse_type.upper())
song.lyrics = str(sxml.extract_xml(), 'utf-8')
# Rebuild the verse order, to convert translated verse tags, which might have been added prior to 1.9.5.
if song.verse_order:
order = CONTROL_CHARS.sub('', song.verse_order).strip().split()
else:
order = []
new_order = []
for verse_def in order:
verse_type = VerseType.tags[
VerseType.from_loose_input(verse_def[0])]
if len(verse_def) > 1:
new_order.append(('%s%s' % (verse_type, verse_def[1:])).upper())
else:
new_order.append(verse_type.upper())
song.verse_order = ' '.join(new_order)
# Check if the verse order contains tags for verses which do not exist.
for order in new_order:
if order not in compare_order:
song.verse_order = ''
break
else:
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1])
for verse in verses])
if isinstance(song.lyrics, bytes):
song.lyrics = str(song.lyrics, encoding='utf8')
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:
@ -484,17 +405,10 @@ def get_encoding(font, font_table, default_encoding, failed=False):
"""
Finds an encoding to use. Asks user, if necessary.
``font``
The number of currently active font.
``font_table``
Dictionary of fonts and respective encodings.
``default_encoding``
The default encoding to use when font_table is empty or no font is used.
``failed``
A boolean indicating whether the previous encoding didn't work.
:param font: The number of currently active font.
:param font_table: Dictionary of fonts and respective encodings.
:param default_encoding: The default encoding to use when font_table is empty or no font is used.
:param failed: A boolean indicating whether the previous encoding didn't work.
"""
encoding = None
if font in font_table:
@ -512,14 +426,11 @@ def strip_rtf(text, default_encoding=None):
"""
This function strips RTF control structures and returns an unicode string.
Thanks to Markus Jarderot (MizardX) for this code, used by permission.
http://stackoverflow.com/questions/188545
Thanks to Markus Jarderot (MizardX) for this code, used by permission. http://stackoverflow.com/questions/188545
``text``
RTF-encoded text, a string.
``default_encoding``
Default encoding to use when no encoding is specified.
:param text: RTF-encoded text, a string.
:param default_encoding: Default encoding to use when no encoding is specified.
:return: A tuple ``(text, encoding)`` where ``text`` is the clean text and ``encoding`` is the detected encoding
"""
# Current font is the font tag we last met.
font = ''
@ -620,20 +531,17 @@ def strip_rtf(text, default_encoding=None):
def delete_song(song_id, song_plugin):
"""
Deletes a song from the database. Media files associated to the song
are removed prior to the deletion of the song.
Deletes a song from the database. Media files associated to the song are removed prior to the deletion of the song.
``song_id``
The ID of the song to delete.
``song_plugin``
The song plugin instance.
:param song_id: The ID of the song to delete.
:param song_plugin: The song plugin instance.
"""
save_path = ''
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
for media_file in media_files:
try:
os.remove(media_file.file_name)
except:
except OSError:
log.exception('Could not remove file: %s', media_file.file_name)
try:
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))

View File

@ -0,0 +1,208 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
"""
import logging
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, HTTPError, build_opener
from html.parser import HTMLParser
from bs4 import BeautifulSoup, NavigableString
from openlp.plugins.songs.lib import Song, VerseType, clean_song, Author
from openlp.plugins.songs.lib.xml import SongXML
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
'Mobile Safari/534.30'
BASE_URL = 'https://mobile.songselect.com'
LOGIN_URL = BASE_URL + '/account/login'
LOGOUT_URL = BASE_URL + '/account/logout'
SEARCH_URL = BASE_URL + '/search/results'
log = logging.getLogger(__name__)
class SongSelectImport(object):
"""
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
with CCLI's SongSelect service and downloads the songs.
"""
def __init__(self, db_manager):
"""
Set up the song select importer
:param db_manager: The song database manager
"""
self.db_manager = db_manager
self.html_parser = HTMLParser()
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
self.opener.addheaders = [('User-Agent', USER_AGENT)]
def login(self, username, password, callback=None):
"""
Log the user into SongSelect. This method takes a username and password, and runs ``callback()`` at various
points which can be used to give the user some form of feedback.
:param username: SongSelect username
:param password: SongSelect password
:param callback: Method to notify of progress.
:return: True on success, False on failure.
"""
if callback:
callback()
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
if callback:
callback()
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
data = urlencode({
'__RequestVerificationToken': token_input['value'],
'UserName': username,
'Password': password,
'RememberMe': 'false'
})
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
if callback:
callback()
return not posted_page.find('input', attrs={'name': '__RequestVerificationToken'})
def logout(self):
"""
Log the user out of SongSelect
"""
self.opener.open(LOGOUT_URL)
def search(self, search_text, max_results, callback=None):
"""
Set up a search.
:param search_text: The text to search for.
:param max_results: Maximum number of results to fetch.
:param callback: A method which is called when each song is found, with the song as a parameter.
:return: List of songs
"""
params = {'allowredirect': 'false', 'SearchTerm': search_text}
current_page = 1
songs = []
while True:
if current_page > 1:
params['page'] = current_page
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
search_results = results_page.find_all('li', 'result pane')
if not search_results:
break
for result in search_results:
song = {
'title': self.html_parser.unescape(result.find('h3').string),
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
'link': BASE_URL + result.find('a')['href']
}
if callback:
callback(song)
songs.append(song)
if len(songs) >= max_results:
break
current_page += 1
return songs
def get_song(self, song, callback=None):
"""
Get the full song from SongSelect
:param song: The song dictionary to update
:param callback: A callback which can be used to indicate progress
:return: The updated song dictionary
"""
if callback:
callback()
try:
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
except (TypeError, HTTPError) as e:
log.exception(u'Could not get song from SongSelect, %s', e)
return None
if callback:
callback()
try:
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
except (TypeError, HTTPError):
log.exception(u'Could not get lyrics from SongSelect')
return None
if callback:
callback()
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
song['copyright'] = self.html_parser.unescape(song['copyright'])
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
song['verses'] = []
verses = lyrics_page.find('section', 'lyrics').find_all('p')
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
for counter in range(len(verses)):
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
for v in verses[counter].contents:
if isinstance(v, NavigableString):
verse['lyrics'] = verse['lyrics'] + v.string
else:
verse['lyrics'] += '\n'
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
song['verses'].append(self.html_parser.unescape(verse))
for counter, author in enumerate(song['authors']):
song['authors'][counter] = self.html_parser.unescape(author)
return song
def save_song(self, song):
"""
Save a song to the database, using the db_manager
:param song:
:return:
"""
db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
song_xml = SongXML()
verse_order = []
for verse in song['verses']:
verse_type, verse_number = verse['label'].split(' ')[:2]
verse_type = VerseType.from_loose_input(verse_type)
verse_number = int(verse_number)
song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
db_song.verse_order = ' '.join(verse_order)
db_song.lyrics = song_xml.extract_xml()
clean_song(self.db_manager, db_song)
self.db_manager.save_object(db_song)
db_song.authors = []
for author_name in song['authors']:
author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name)
if not author:
author = Author.populate(first_name=author_name.rsplit(' ', 1)[0],
last_name=author_name.rsplit(' ', 1)[1],
display_name=author_name)
db_song.authors.append(author)
self.db_manager.save_object(db_song)
return db_song

View File

@ -38,11 +38,13 @@ import sqlite3
from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
from openlp.core.common import UiStrings, Registry, translate
from openlp.core.lib import Plugin, StringContent, build_icon
from openlp.core.lib.db import Manager
from openlp.core.lib.ui import create_action
from openlp.core.utils.actions import ActionList
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
from openlp.plugins.songs.forms.songselectform import SongSelectForm
from openlp.plugins.songs.lib import clean_song, upgrade
from openlp.plugins.songs.lib.db import init_schema, Song
from openlp.plugins.songs.lib.mediaitem import SongSearch
@ -50,27 +52,29 @@ from openlp.plugins.songs.lib.importer import SongFormat
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.lib.songstab import SongsTab
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
log = logging.getLogger(__name__)
__default_settings__ = {'songs/db type': 'sqlite',
'songs/last search type': SongSearch.Entire,
'songs/last import type': SongFormat.OpenLyrics,
'songs/update service on edit': False,
'songs/search as type': False,
'songs/add song from service': True,
'songs/display songbar': True,
'songs/last directory import': '',
'songs/last directory export': ''
}
__default_settings__ = {
'songs/db type': 'sqlite',
'songs/last search type': SongSearch.Entire,
'songs/last import type': SongFormat.OpenLyrics,
'songs/update service on edit': False,
'songs/search as type': False,
'songs/add song from service': True,
'songs/display songbar': True,
'songs/last directory import': '',
'songs/last directory export': '',
'songs/songselect username': '',
'songs/songselect password': '',
'songs/songselect searches': ''
}
class SongsPlugin(Plugin):
"""
This is the number 1 plugin, if importance were placed on any plugins. This plugin enables the user to create,
edit and display songs. Songs are divided into verses, and the verse order can be specified. Authors, topics and
song books can be assigned to songs as well.
This plugin enables the user to create, edit and display songs. Songs are divided into verses, and the verse order
can be specified. Authors, topics and song books can be assigned to songs as well.
"""
log.info('Song Plugin loaded')
@ -83,6 +87,7 @@ class SongsPlugin(Plugin):
self.weight = -10
self.icon_path = ':/plugins/plugin_songs.png'
self.icon = build_icon(self.icon_path)
self.songselect_form = None
def check_pre_conditions(self):
"""
@ -92,10 +97,11 @@ class SongsPlugin(Plugin):
def initialise(self):
"""
Lets Initialise the plugin
Initialise the plugin
"""
log.info('Songs Initialising')
super(SongsPlugin, self).initialise()
self.songselect_form = SongSelectForm(Registry().get('main_window'), self, self.manager)
self.song_import_item.setVisible(True)
self.song_export_item.setVisible(True)
self.tools_reindex_item.setVisible(True)
@ -119,12 +125,18 @@ class SongsPlugin(Plugin):
tooltip=translate('SongsPlugin', 'Import songs using the import wizard.'),
triggers=self.on_song_import_item_clicked)
import_menu.addAction(self.song_import_item)
self.import_songselect_item = create_action(
import_menu, 'import_songselect_item', text=translate('SongsPlugin', 'CCLI SongSelect'),
statustip=translate('SongsPlugin', 'Import songs from CCLI\'s SongSelect service.'),
triggers=self.on_import_songselect_item_triggered
)
import_menu.addAction(self.import_songselect_item)
def add_export_menu_Item(self, export_menu):
"""
Give the Songs plugin the opportunity to add items to the **Export** menu.
:param export_menu: The actual **Export** menu item, so that your actions can use it as their parent.
:param export_menu: The actual **Export** menu item, so that your actions can use it as their parent.
"""
# Main song import menu item - will eventually be the only one
self.song_export_item = create_action(
@ -179,29 +191,42 @@ class SongsPlugin(Plugin):
"""
DuplicateSongRemovalForm(self).exec_()
def on_import_songselect_item_triggered(self):
"""
Run the SongSelect importer.
"""
self.songselect_form.exec_()
self.media_item.on_search_text_button_clicked()
def on_song_import_item_clicked(self):
"""
The song import option has been selected
Run the song import wizard.
"""
if self.media_item:
self.media_item.on_import_click()
def on_song_export_item_clicked(self):
"""
The song export option has been selected
Run the song export wizard.
"""
if self.media_item:
self.media_item.on_export_click()
def about(self):
"""
Provides information for the plugin manager to display.
:return: A translatable string with some basic information about the Songs plugin
"""
return translate('SongsPlugin', '<strong>Songs Plugin</strong>'
'<br />The songs plugin provides the ability to display and manage songs.')
'<br />The songs plugin provides the ability to display and manage songs.')
def uses_theme(self, theme):
"""
Called to find out if the song plugin is currently using a theme.
Returns True if the theme is being used, otherwise returns False.
:param theme: The theme to check for usage
:return: True if the theme is being used, otherwise returns False
"""
if self.manager.get_all_objects(Song, Song.theme_name == theme):
return True

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

View File

@ -61,6 +61,8 @@
<file>general_email.png</file>
<file>general_revert.png</file>
<file>general_clone.png</file>
<file>general_find.png</file>
<file>general_back.png</file>
</qresource>
<qresource prefix="slides">
<file>slide_close.png</file>

View File

@ -1,3 +1,7 @@
# -*- coding: utf-8 -*-
"""
Base directory for tests
"""
import sip
sip.setapi('QDate', 2)
sip.setapi('QDateTime', 2)
@ -11,9 +15,11 @@ import sys
from PyQt4 import QtGui
if sys.version_info[1] >= 3:
from unittest.mock import MagicMock, patch, mock_open
from unittest.mock import MagicMock, patch, mock_open, call
else:
from mock import MagicMock, patch, mock_open
from mock import MagicMock, patch, mock_open, call
# Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a QApplication.
application = QtGui.QApplication([])
__all__ = ['MagicMock', 'patch', 'mock_open', 'call', 'application']

View File

@ -38,20 +38,20 @@ from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..', 'resources', 'songbeamersongs'))
SONG_TEST_DATA = {'Lobsinget dem Herrn.sng':
{'title': 'GL 1 - Lobsinget dem Herrn',
'verses':
[('1. Lobsinget dem Herrn,\no preiset Ihn gern!\n'
'Anbetung und Lob Ihm gebühret.\n', 'v'),
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\n'
'zu sterben für unsere Sünden!\n', 'v'),
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\n'
'Er sitzet zur Rechten des Vaters.\n', 'v'),
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\n'
'bis Er uns zur Herrlichket führet!\n\n', 'v')],
'song_book_name': 'Glaubenslieder I',
'song_number': "1"}
}
SONG_TEST_DATA = {
'Lobsinget dem Herrn.sng': {
'title': 'GL 1 - Lobsinget dem Herrn',
'verses': [
('1. Lobsinget dem Herrn,\no preiset Ihn gern!\nAnbetung und Lob Ihm gebühret.\n', 'v'),
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\nzu sterben für unsere Sünden!\n', 'v'),
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\nEr sitzet zur Rechten des Vaters.\n', 'v'),
('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"
}
}
class TestSongBeamerImport(TestCase):
"""

View File

@ -0,0 +1,382 @@
# -*- 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 CCLI SongSelect importer.
"""
from unittest import TestCase
from urllib.error import URLError
from openlp.plugins.songs.lib import Author, Song
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL
from tests.functional import MagicMock, patch, call
class TestSongSelect(TestCase):
"""
Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
"""
def constructor_test(self):
"""
Test that constructing a basic SongSelectImport object works correctly
"""
# GIVEN: The SongSelectImporter class and a mocked out build_opener
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
# WHEN: An object is instantiated
importer = SongSelectImport(None)
# THEN: The object should have the correct properties
self.assertIsNone(importer.db_manager, 'The db_manager should be None')
self.assertIsNotNone(importer.html_parser, 'There should be a valid html_parser object')
self.assertIsNotNone(importer.opener, 'There should be a valid opener object')
self.assertEqual(1, mocked_build_opener.call_count, 'The build_opener method should have been called once')
def login_fails_test(self):
"""
Test that when logging in to SongSelect fails, the login method returns False
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock()
mocked_login_page.find.return_value = {'value': 'blah'}
MockedBeautifulSoup.return_value = mocked_login_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
self.assertFalse(result, 'The login method should have returned False')
def login_succeeds_test(self):
"""
Test that when logging in to SongSelect succeeds, the login method returns True
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_login_page = MagicMock()
mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
MockedBeautifulSoup.return_value = mocked_login_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times')
self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice')
self.assertTrue(result, 'The login method should have returned True')
def logout_test(self):
"""
Test that when the logout method is called, it logs the user out of SongSelect
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
importer.logout()
# THEN: The opener is called once with the logout url
self.assertEqual(1, mocked_opener.open.call_count, 'opener should have been called once')
mocked_opener.open.assert_called_with(LOGOUT_URL)
def search_returns_no_results_test(self):
"""
Test that when the search finds no results, it simply returns an empty list
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.return_value = []
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(0, mock_callback.call_count, 'callback should not have been called')
self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once')
self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
self.assertEqual([], results, 'The search method should have returned an empty list')
def search_returns_two_results_test(self):
"""
Test that when the search finds 2 results, it simply returns a list with 2 results
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result
mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
# second search result
mocked_result2 = MagicMock()
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
# rest of the stuff
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2], []]
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
expected_list = [
{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}
]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
def search_reaches_max_results_test(self):
"""
Test that when the search finds MAX (2) results, it simply returns a list with those (2)
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener, \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
# first search result
mocked_result1 = MagicMock()
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
# second search result
mocked_result2 = MagicMock()
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
# third search result
mocked_result3 = MagicMock()
mocked_result3.find.side_effect = [MagicMock(string='Title 3'), {'href': '/url3'}]
mocked_result3.find_all.return_value = [MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2')]
# rest of the stuff
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_results_page = MagicMock()
mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2, mocked_result3], []]
MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail
results = importer.search('text', 2, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
mocked_results_page.find_all.assert_called_with('li', 'result pane')
expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
def get_song_page_raises_exception_test(self):
"""
Test that when BeautifulSoup gets a bad song page the get_song() method returns None
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener') as mocked_build_opener:
mocked_opener = MagicMock()
mocked_build_opener.return_value = mocked_opener
mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known')
mocked_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: get_song is called
result = importer.get_song({'link': 'link'}, callback=mocked_callback)
# THEN: The callback should have been called once and None should be returned
mocked_callback.assert_called_with()
self.assertIsNone(result, 'The get_song() method should have returned None')
def get_song_lyrics_raise_exception_test(self):
"""
Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener'), \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')]
mocked_callback = MagicMock()
importer = SongSelectImport(None)
# WHEN: get_song is called
result = importer.get_song({'link': 'link'}, callback=mocked_callback)
# THEN: The callback should have been called twice and None should be returned
self.assertEqual(2, mocked_callback.call_count, 'The callback should have been called twice')
self.assertIsNone(result, 'The get_song() method should have returned None')
def get_song_test(self):
"""
Test that the get_song() method returns the correct song details
"""
# GIVEN: A bunch of mocked out stuff and an importer object
with patch('openlp.plugins.songs.lib.songselect.build_opener'), \
patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') as MockedBeautifulSoup:
mocked_song_page = MagicMock()
mocked_copyright = MagicMock()
mocked_copyright.find_all.return_value = [MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2')]
mocked_song_page.find.side_effect = [
mocked_copyright,
MagicMock(find=MagicMock(string='CCLI: 123456'))
]
mocked_lyrics_page = MagicMock()
mocked_find_all = MagicMock()
mocked_find_all.side_effect = [
[
MagicMock(contents='The Lord told Noah: there\'s gonna be a floody, floody'),
MagicMock(contents='So, rise and shine, and give God the glory, glory'),
MagicMock(contents='The Lord told Noah to build him an arky, arky')
],
[MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2')]
]
mocked_lyrics_page.find.return_value = MagicMock(find_all=mocked_find_all)
MockedBeautifulSoup.side_effect = [mocked_song_page, mocked_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None)
fake_song = {'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url'}
# WHEN: get_song is called
result = importer.get_song(fake_song, callback=mocked_callback)
# THEN: The callback should have been called three times and the song should be returned
self.assertEqual(3, mocked_callback.call_count, 'The callback should have been called twice')
self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary')
self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice')
self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice')
self.assertEqual([call('section', 'lyrics'), call('section', 'lyrics')],
mocked_lyrics_page.find.call_args_list,
'The find() method should have been called with the right arguments')
self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list,
'The find_all() method should have been called with the right arguments')
self.assertIn('copyright', result, 'The returned song should have a copyright')
self.assertIn('ccli_number', result, 'The returned song should have a CCLI number')
self.assertIn('verses', result, 'The returned song should have verses')
self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')
def save_song_new_author_test(self):
"""
Test that saving a song with a new author performs the correct actions
"""
# GIVEN: A song to save, and some mocked out objects
with patch('openlp.plugins.songs.lib.songselect.clean_song') as mocked_clean_song, \
patch('openlp.plugins.songs.lib.songselect.Author') as MockedAuthor:
song_dict = {
'title': 'Arky Arky',
'authors': ['Public Domain'],
'verses': [
{'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456'
}
MockedAuthor.display_name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = None
importer = SongSelectImport(mocked_db_manager)
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the mocked_db_manager should have been called
self.assertIsInstance(result, Song, 'The returned value should be a Song object')
mocked_clean_song.assert_called_with(mocked_db_manager, result)
self.assertEqual(2, mocked_db_manager.save_object.call_count,
'The save_object() method should have been called twice')
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
MockedAuthor.populate.assert_called_with(first_name='Public', last_name='Domain',
display_name='Public Domain')
self.assertEqual(1, len(result.authors), 'There should only be one author')
def save_song_existing_author_test(self):
"""
Test that saving a song with an existing author performs the correct actions
"""
# GIVEN: A song to save, and some mocked out objects
with patch('openlp.plugins.songs.lib.songselect.clean_song') as mocked_clean_song, \
patch('openlp.plugins.songs.lib.songselect.Author') as MockedAuthor:
song_dict = {
'title': 'Arky Arky',
'authors': ['Public Domain'],
'verses': [
{'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
{'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
{'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
],
'copyright': 'Public Domain',
'ccli_number': '123456'
}
MockedAuthor.display_name.__eq__.return_value = False
mocked_db_manager = MagicMock()
mocked_db_manager.get_object_filtered.return_value = MagicMock()
importer = SongSelectImport(mocked_db_manager)
# WHEN: The song is saved to the database
result = importer.save_song(song_dict)
# THEN: The return value should be a Song class and the mocked_db_manager should have been called
self.assertIsInstance(result, Song, 'The returned value should be a Song object')
mocked_clean_song.assert_called_with(mocked_db_manager, result)
self.assertEqual(2, mocked_db_manager.save_object.call_count,
'The save_object() method should have been called twice')
mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
self.assertEqual(0, MockedAuthor.populate.call_count, 'A new author should not have been instantiated')
self.assertEqual(1, len(result.authors), 'There should only be one author')

View File

@ -41,6 +41,7 @@ from tests.functional import patch, MagicMock
TEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'songshowplussongs'))
class TestSongShowPlusFileImport(SongImportTestHelper):
def __init__(self, *args, **kwargs):
self.importer_class_name = 'SongShowPlusImport'
@ -48,10 +49,13 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
def test_song_import(self):
test_import = self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
test_import = self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
"""
Test that loading a SongShow Plus file works correctly on various files
"""
self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
class TestSongShowPlusImport(TestCase):

31
tests/helpers/__init__.py Normal file
View File

@ -0,0 +1,31 @@
# -*- 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 #
###############################################################################
"""
The :mod:`~tests.helpers` module provides helper classes for use in the tests.
"""

View File

@ -35,6 +35,7 @@ from unittest import TestCase
from tests.functional import patch, MagicMock
class SongImportTestHelper(TestCase):
"""
This class is designed to be a helper class to reduce repition when testing the import of song files.