forked from openlp/openlp
Head
This commit is contained in:
commit
e78dac1edf
@ -134,4 +134,4 @@ from .registrymixin import RegistryMixin
|
|||||||
from .uistrings import UiStrings
|
from .uistrings import UiStrings
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
from .applocation import AppLocation
|
from .applocation import AppLocation
|
||||||
|
from .historycombobox import HistoryComboBox
|
||||||
|
91
openlp/core/common/historycombobox.py
Normal file
91
openlp/core/common/historycombobox.py
Normal 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())]
|
@ -29,6 +29,7 @@
|
|||||||
"""
|
"""
|
||||||
The :mod:`ui` module provides the core user interface for OpenLP
|
The :mod:`ui` module provides the core user interface for OpenLP
|
||||||
"""
|
"""
|
||||||
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
|
|
||||||
class HideMode(object):
|
class HideMode(object):
|
||||||
@ -77,6 +78,29 @@ class DisplayControllerType(object):
|
|||||||
Plugin = 2
|
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 .firsttimeform import FirstTimeForm
|
||||||
from .firsttimelanguageform import FirstTimeLanguageForm
|
from .firsttimelanguageform import FirstTimeLanguageForm
|
||||||
from .themelayoutform import ThemeLayoutForm
|
from .themelayoutform import ThemeLayoutForm
|
||||||
@ -101,8 +125,8 @@ from .mediadockmanager import MediaDockManager
|
|||||||
from .servicemanager import ServiceManager
|
from .servicemanager import ServiceManager
|
||||||
from .thememanager import ThemeManager
|
from .thememanager import ThemeManager
|
||||||
|
|
||||||
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager',
|
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
|
||||||
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm',
|
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm',
|
||||||
'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm',
|
'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay',
|
||||||
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
|
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
|
||||||
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController']
|
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget']
|
||||||
|
@ -119,8 +119,10 @@ class Ui_MainWindow(object):
|
|||||||
self.recent_files_menu = QtGui.QMenu(self.file_menu)
|
self.recent_files_menu = QtGui.QMenu(self.file_menu)
|
||||||
self.recent_files_menu.setObjectName('recentFilesMenu')
|
self.recent_files_menu.setObjectName('recentFilesMenu')
|
||||||
self.file_import_menu = QtGui.QMenu(self.file_menu)
|
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_import_menu.setObjectName('file_import_menu')
|
||||||
self.file_export_menu = QtGui.QMenu(self.file_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')
|
self.file_export_menu.setObjectName('file_export_menu')
|
||||||
# View Menu
|
# View Menu
|
||||||
self.view_menu = QtGui.QMenu(self.menu_bar)
|
self.view_menu = QtGui.QMenu(self.menu_bar)
|
||||||
@ -305,10 +307,10 @@ class Ui_MainWindow(object):
|
|||||||
'searchShortcut', can_shortcuts=True,
|
'searchShortcut', can_shortcuts=True,
|
||||||
category=translate('OpenLP.MainWindow', 'General'),
|
category=translate('OpenLP.MainWindow', 'General'),
|
||||||
triggers=self.on_search_shortcut_triggered)
|
triggers=self.on_search_shortcut_triggered)
|
||||||
add_actions(self.file_import_menu, (self.settings_import_item, None, self.import_theme_item,
|
add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item,
|
||||||
self.import_language_item))
|
self.import_language_item, None))
|
||||||
add_actions(self.file_export_menu, (self.settings_export_item, None, self.export_theme_item,
|
add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item,
|
||||||
self.export_language_item))
|
self.export_language_item, None))
|
||||||
add_actions(self.file_menu, (self.file_new_item, self.file_open_item,
|
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_save_item, self.file_save_as_item, self.recent_files_menu.menuAction(), None,
|
||||||
self.file_import_menu.menuAction(), self.file_export_menu.menuAction(), None,
|
self.file_import_menu.menuAction(), self.file_export_menu.menuAction(), None,
|
||||||
|
@ -32,6 +32,7 @@ from PyQt4 import QtCore, QtGui
|
|||||||
from openlp.core.common import UiStrings, translate
|
from openlp.core.common import UiStrings, translate
|
||||||
from openlp.core.lib import build_icon
|
from openlp.core.lib import build_icon
|
||||||
from openlp.core.lib.ui import create_button_box, create_button
|
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
|
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.setInsertPolicy(QtGui.QComboBox.NoInsert)
|
||||||
combo_box.setObjectName(name)
|
combo_box.setObjectName(name)
|
||||||
return combo_box
|
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()
|
|
||||||
|
252
openlp/plugins/songs/forms/songselectdialog.py
Normal file
252
openlp/plugins/songs/forms/songselectdialog.py
Normal 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'))
|
387
openlp/plugins/songs/forms/songselectform.py
Executable file
387
openlp/plugins/songs/forms/songselectform.py
Executable 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
|
@ -53,64 +53,41 @@ APOSTROPHE = re.compile('[\'`’ʻ′]', re.UNICODE)
|
|||||||
# \# - where # is a single non-alpha character, representing a special symbol
|
# \# - where # is a single non-alpha character, representing a special symbol
|
||||||
# { or } - marking the beginning/end of a group
|
# { or } - marking the beginning/end of a group
|
||||||
# a run of characters without any \ { } or end-of-line
|
# 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.
|
# RTF control words which specify a "destination" to be ignored.
|
||||||
DESTINATIONS = frozenset((
|
DESTINATIONS = frozenset((
|
||||||
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor',
|
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor', 'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref',
|
||||||
'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref', 'atntime',
|
'atntime', 'atrfend', 'atrfstart', 'author', 'background', 'bkmkend', 'bkmkstart', 'blipuid', 'buptim', 'category',
|
||||||
'atrfend', 'atrfstart', 'author', 'background', 'bkmkend',
|
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim', 'datafield', 'datastore', 'defchp', 'defpap',
|
||||||
'bkmkstart', 'blipuid', 'buptim', 'category',
|
'do', 'doccomm', 'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname', 'falt', 'fchars', 'ffdeftext',
|
||||||
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim',
|
'ffentrymcr', 'ffexitmcr', 'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext', 'file', 'filetbl', 'fldinst',
|
||||||
'datafield', 'datastore', 'defchp', 'defpap', 'do', 'doccomm',
|
'fldtype', 'fname', 'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr', 'footnote', 'formfield',
|
||||||
'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname',
|
'ftncn', 'ftnsep', 'ftnsepc', 'g', 'generator', 'gridtbl', 'header', 'headerf', 'headerl', 'headerr', 'hl', 'hlfr',
|
||||||
'falt', 'fchars', 'ffdeftext', 'ffentrymcr', 'ffexitmcr',
|
'hlinkbase', 'hlloc', 'hlsrc', 'hsv', 'htmltag', 'info', 'keycode', 'keywords', 'latentstyles', 'lchars',
|
||||||
'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext',
|
'levelnumbers', 'leveltext', 'lfolevel', 'linkval', 'list', 'listlevel', 'listname', 'listoverride',
|
||||||
'file', 'filetbl', 'fldinst', 'fldtype', 'fname',
|
'listoverridetable', 'listpicture', 'liststylename', 'listtable', 'listtext', 'lsdlockedexcept', 'macc', 'maccPr',
|
||||||
'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr',
|
'mailmerge', 'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr', 'mbaseJc', 'mbegChr', 'mborderBox',
|
||||||
'footnote', 'formfield', 'ftncn', 'ftnsep', 'ftnsepc', 'g',
|
'mborderBoxPr', 'mbox', 'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide', 'mden', 'mdiff', 'mdPr',
|
||||||
'generator', 'gridtbl', 'header', 'headerf', 'headerl',
|
'me', 'mendChr', 'meqArr', 'meqArrPr', 'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr', 'mgroupChrPr',
|
||||||
'headerr', 'hl', 'hlfr', 'hlinkbase', 'hlloc', 'hlsrc', 'hsv',
|
'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight', 'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow', 'mlimlowPr',
|
||||||
'htmltag', 'info', 'keycode', 'keywords', 'latentstyles',
|
'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname', 'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
|
||||||
'lchars', 'levelnumbers', 'leveltext', 'lfolevel', 'linkval',
|
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs', 'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
|
||||||
'list', 'listlevel', 'listname', 'listoverride',
|
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname', 'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
|
||||||
'listoverridetable', 'listpicture', 'liststylename', 'listtable',
|
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag', 'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr',
|
||||||
'listtext', 'lsdlockedexcept', 'macc', 'maccPr', 'mailmerge',
|
'mnoBreak', 'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr', 'mopEmu', 'mphant', 'mphantPr', 'mplcHide',
|
||||||
'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr',
|
'mpos', 'mr', 'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre', 'msPrePr', 'msSub', 'msSubPr',
|
||||||
'mbaseJc', 'mbegChr', 'mborderBox', 'mborderBoxPr', 'mbox',
|
'msSubSup', 'msSubSupPr', 'msSup', 'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV', 'msub',
|
||||||
'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide',
|
'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype', 'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
|
||||||
'mden', 'mdiff', 'mdPr', 'me', 'mendChr', 'meqArr', 'meqArrPr',
|
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile', 'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
|
||||||
'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr',
|
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops', 'oldsprops', 'oldtprops', 'oleclsid', 'operator',
|
||||||
'mgroupChrPr', 'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight',
|
'panose', 'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict', 'pn', 'pnseclvl', 'pntext', 'pntxta',
|
||||||
'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow',
|
'pntxtb', 'printim', 'private', 'propname', 'protend', 'protstart', 'protusertbl', 'pxe', 'result', 'revtbl',
|
||||||
'mlimlowPr', 'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname',
|
'revtim', 'rsidtbl', 'rxe', 'shp', 'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp', 'staticval',
|
||||||
'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
|
'stylesheet', 'subject', 'sv', 'svb', 'tc', 'template', 'themedata', 'title', 'txe', 'ud', 'upr', 'userprops',
|
||||||
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs',
|
'wgrffmtfilter', 'windowcaption', 'writereservation', 'writereservhash', 'xe', 'xform', 'xmlattrname',
|
||||||
'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
|
'xmlattrvalue', 'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'
|
||||||
'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.
|
# Translation of some special characters.
|
||||||
SPECIAL_CHARS = {
|
SPECIAL_CHARS = {
|
||||||
'\n': '\n',
|
'\n': '\n',
|
||||||
@ -142,7 +119,8 @@ SPECIAL_CHARS = {
|
|||||||
'ltrmark': '\u200E',
|
'ltrmark': '\u200E',
|
||||||
'rtlmark': '\u200F',
|
'rtlmark': '\u200F',
|
||||||
'zwj': '\u200D',
|
'zwj': '\u200D',
|
||||||
'zwnj': '\u200C'}
|
'zwnj': '\u200C'
|
||||||
|
}
|
||||||
CHARSET_MAPPING = {
|
CHARSET_MAPPING = {
|
||||||
'0': 'cp1252',
|
'0': 'cp1252',
|
||||||
'128': 'cp932',
|
'128': 'cp932',
|
||||||
@ -156,7 +134,8 @@ CHARSET_MAPPING = {
|
|||||||
'186': 'cp1257',
|
'186': 'cp1257',
|
||||||
'204': 'cp1251',
|
'204': 'cp1251',
|
||||||
'222': 'cp874',
|
'222': 'cp874',
|
||||||
'238': 'cp1250'}
|
'238': 'cp1250'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VerseType(object):
|
class VerseType(object):
|
||||||
@ -171,14 +150,7 @@ class VerseType(object):
|
|||||||
Ending = 5
|
Ending = 5
|
||||||
Other = 6
|
Other = 6
|
||||||
|
|
||||||
names = [
|
names = ['Verse', 'Chorus', 'Bridge', 'Pre-Chorus', 'Intro', 'Ending', 'Other']
|
||||||
'Verse',
|
|
||||||
'Chorus',
|
|
||||||
'Bridge',
|
|
||||||
'Pre-Chorus',
|
|
||||||
'Intro',
|
|
||||||
'Ending',
|
|
||||||
'Other']
|
|
||||||
tags = [name[0].lower() for name in names]
|
tags = [name[0].lower() for name in names]
|
||||||
|
|
||||||
translated_names = [
|
translated_names = [
|
||||||
@ -199,6 +171,7 @@ class VerseType(object):
|
|||||||
|
|
||||||
:param verse_tag: The string to return a VerseType for
|
:param verse_tag: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: A translated UPPERCASE tag
|
||||||
"""
|
"""
|
||||||
verse_tag = verse_tag[0].lower()
|
verse_tag = verse_tag[0].lower()
|
||||||
for num, tag in enumerate(VerseType.tags):
|
for num, tag in enumerate(VerseType.tags):
|
||||||
@ -216,6 +189,7 @@ class VerseType(object):
|
|||||||
|
|
||||||
:param verse_tag: The string to return a VerseType for
|
:param verse_tag: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: Translated name for the given tag
|
||||||
"""
|
"""
|
||||||
verse_tag = verse_tag[0].lower()
|
verse_tag = verse_tag[0].lower()
|
||||||
for num, tag in enumerate(VerseType.tags):
|
for num, tag in enumerate(VerseType.tags):
|
||||||
@ -233,6 +207,7 @@ class VerseType(object):
|
|||||||
|
|
||||||
:param verse_tag: The string to return a VerseType for
|
:param verse_tag: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: A VerseType of the tag
|
||||||
"""
|
"""
|
||||||
verse_tag = verse_tag[0].lower()
|
verse_tag = verse_tag[0].lower()
|
||||||
for num, tag in enumerate(VerseType.tags):
|
for num, tag in enumerate(VerseType.tags):
|
||||||
@ -250,6 +225,7 @@ class VerseType(object):
|
|||||||
|
|
||||||
:param verse_tag: The string to return a VerseType for
|
:param verse_tag: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: The VerseType of a translated tag
|
||||||
"""
|
"""
|
||||||
verse_tag = verse_tag[0].lower()
|
verse_tag = verse_tag[0].lower()
|
||||||
for num, tag in enumerate(VerseType.translated_tags):
|
for num, tag in enumerate(VerseType.translated_tags):
|
||||||
@ -266,7 +242,8 @@ class VerseType(object):
|
|||||||
Return the VerseType for a given string
|
Return the VerseType for a given string
|
||||||
|
|
||||||
:param verse_name: The string to return a VerseType for
|
:param verse_name: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: The VerseType determined from the string
|
||||||
"""
|
"""
|
||||||
verse_name = verse_name.lower()
|
verse_name = verse_name.lower()
|
||||||
for num, name in enumerate(VerseType.names):
|
for num, name in enumerate(VerseType.names):
|
||||||
@ -280,6 +257,7 @@ class VerseType(object):
|
|||||||
Return the VerseType for a given string
|
Return the VerseType for a given string
|
||||||
|
|
||||||
:param 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()
|
verse_name = verse_name.lower()
|
||||||
for num, translation in enumerate(VerseType.translated_names):
|
for num, translation in enumerate(VerseType.translated_names):
|
||||||
@ -293,6 +271,7 @@ class VerseType(object):
|
|||||||
|
|
||||||
:param verse_name: The string to return a VerseType for
|
:param verse_name: The string to return a VerseType for
|
||||||
:param default: Default return value if no matching tag is found
|
:param default: Default return value if no matching tag is found
|
||||||
|
:return: A VerseType
|
||||||
"""
|
"""
|
||||||
if len(verse_name) > 1:
|
if len(verse_name) > 1:
|
||||||
verse_index = VerseType.from_translated_string(verse_name)
|
verse_index = VerseType.from_translated_string(verse_name)
|
||||||
@ -313,16 +292,14 @@ def retrieve_windows_encoding(recommendation=None):
|
|||||||
passed to this method as a recommendation, and user confirmation to return an encoding.
|
passed to this method as a recommendation, and user confirmation to return an encoding.
|
||||||
|
|
||||||
:param 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
|
# map chardet result to compatible windows standard code page
|
||||||
codepage_mapping = {
|
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874', 'SHIFT_JIS': 'cp932', 'GB2312': 'cp936',
|
||||||
'IBM866': 'cp866', 'TIS-620': 'cp874',
|
'HZ-GB-2312': 'cp936', 'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
|
||||||
'SHIFT_JIS': 'cp932', 'GB2312': 'cp936', 'HZ-GB-2312': 'cp936',
|
'windows-1250': 'cp1250', 'windows-1251': 'cp1251', 'windows-1252': 'cp1252',
|
||||||
'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
|
'ISO-8859-7': 'cp1253', 'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
|
||||||
'windows-1250': 'cp1250', 'windows-1251': 'cp1251',
|
'windows-1255': 'cp1255'}
|
||||||
'windows-1252': 'cp1252', 'ISO-8859-7': 'cp1253',
|
|
||||||
'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
|
|
||||||
'windows-1255': 'cp1255'}
|
|
||||||
if recommendation in codepage_mapping:
|
if recommendation in codepage_mapping:
|
||||||
recommendation = codepage_mapping[recommendation]
|
recommendation = codepage_mapping[recommendation]
|
||||||
|
|
||||||
@ -341,7 +318,8 @@ def retrieve_windows_encoding(recommendation=None):
|
|||||||
('cp950', translate('SongsPlugin', 'Traditional Chinese (CP-950)')),
|
('cp950', translate('SongsPlugin', 'Traditional Chinese (CP-950)')),
|
||||||
('cp1254', translate('SongsPlugin', 'Turkish (CP-1254)')),
|
('cp1254', translate('SongsPlugin', 'Turkish (CP-1254)')),
|
||||||
('cp1258', translate('SongsPlugin', 'Vietnam (CP-1258)')),
|
('cp1258', translate('SongsPlugin', 'Vietnam (CP-1258)')),
|
||||||
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))]
|
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))
|
||||||
|
]
|
||||||
recommended_index = -1
|
recommended_index = -1
|
||||||
if recommendation:
|
if recommendation:
|
||||||
for index in range(len(encodings)):
|
for index in range(len(encodings)):
|
||||||
@ -350,15 +328,18 @@ def retrieve_windows_encoding(recommendation=None):
|
|||||||
break
|
break
|
||||||
if recommended_index > -1:
|
if recommended_index > -1:
|
||||||
choice = QtGui.QInputDialog.getItem(
|
choice = QtGui.QInputDialog.getItem(
|
||||||
None, translate('SongsPlugin', 'Character Encoding'),
|
None,
|
||||||
translate('SongsPlugin', 'The codepage setting is responsible\nfor the correct character '
|
translate('SongsPlugin', 'Character Encoding'),
|
||||||
'representation.\nUsually you are fine with the preselected choice.'),
|
translate('SongsPlugin', 'The codepage setting is responsible\n'
|
||||||
|
'for the correct character representation.\n'
|
||||||
|
'Usually you are fine with the preselected choice.'),
|
||||||
[pair[1] for pair in encodings], recommended_index, False)
|
[pair[1] for pair in encodings], recommended_index, False)
|
||||||
else:
|
else:
|
||||||
choice = QtGui.QInputDialog.getItem(
|
choice = QtGui.QInputDialog.getItem(
|
||||||
None, translate('SongsPlugin', 'Character Encoding'),
|
None,
|
||||||
translate('SongsPlugin', 'Please choose the character encoding.\nThe encoding is responsible for the '
|
translate('SongsPlugin', 'Character Encoding'),
|
||||||
'correct character representation.'),
|
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)
|
[pair[1] for pair in encodings], 0, False)
|
||||||
if not choice[1]:
|
if not choice[1]:
|
||||||
return None
|
return None
|
||||||
@ -368,6 +349,9 @@ def retrieve_windows_encoding(recommendation=None):
|
|||||||
def clean_string(string):
|
def clean_string(string):
|
||||||
"""
|
"""
|
||||||
Strips punctuation from the passed string to assist searching.
|
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()
|
return WHITESPACE.sub(' ', APOSTROPHE.sub('', string)).lower()
|
||||||
|
|
||||||
@ -375,6 +359,9 @@ def clean_string(string):
|
|||||||
def clean_title(title):
|
def clean_title(title):
|
||||||
"""
|
"""
|
||||||
Cleans the song title by removing Unicode control chars groups C0 & C1, as well as any trailing spaces.
|
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()
|
return CONTROL_CHARS.sub('', title).rstrip()
|
||||||
|
|
||||||
@ -384,7 +371,7 @@ 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
|
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.
|
clean ups. This should always called when a new song is added or changed.
|
||||||
|
|
||||||
:param manager: The song's manager.
|
:param manager: The song database manager object.
|
||||||
:param song: The song object.
|
:param song: The song object.
|
||||||
"""
|
"""
|
||||||
from .xml import SongXML
|
from .xml import SongXML
|
||||||
@ -398,54 +385,10 @@ def clean_song(manager, song):
|
|||||||
else:
|
else:
|
||||||
song.alternate_title = ''
|
song.alternate_title = ''
|
||||||
song.search_title = clean_string(song.title) + '@' + clean_string(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 isinstance(song.lyrics, bytes):
|
||||||
if song.lyrics.find('<lyrics language="en">') != -1:
|
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||||
# Remove the old "language" attribute from lyrics tag (prior to 1.9.5). This is not very important, but this
|
verses = SongXML().get_verses(song.lyrics)
|
||||||
# keeps the database clean. This can be removed when everybody has cleaned his songs.
|
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
|
||||||
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])
|
|
||||||
|
|
||||||
# The song does not have any author, add one.
|
# The song does not have any author, add one.
|
||||||
if not song.authors:
|
if not song.authors:
|
||||||
name = SongStrings.AuthorUnknown
|
name = SongStrings.AuthorUnknown
|
||||||
@ -482,12 +425,11 @@ def strip_rtf(text, default_encoding=None):
|
|||||||
"""
|
"""
|
||||||
This function strips RTF control structures and returns an unicode string.
|
This function strips RTF control structures and returns an unicode string.
|
||||||
|
|
||||||
Thanks to Markus Jarderot (MizardX) for this code, used by permission.
|
Thanks to Markus Jarderot (MizardX) for this code, used by permission. http://stackoverflow.com/questions/188545
|
||||||
|
|
||||||
http://stackoverflow.com/questions/188545
|
|
||||||
|
|
||||||
:param text: RTF-encoded text, a string.
|
:param text: RTF-encoded text, a string.
|
||||||
:param default_encoding: Default encoding to use when no encoding is specified.
|
: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.
|
# Current font is the font tag we last met.
|
||||||
font = ''
|
font = ''
|
||||||
@ -588,17 +530,17 @@ def strip_rtf(text, default_encoding=None):
|
|||||||
|
|
||||||
def delete_song(song_id, song_plugin):
|
def delete_song(song_id, song_plugin):
|
||||||
"""
|
"""
|
||||||
Deletes a song from the database. Media files associated to the song
|
Deletes a song from the database. Media files associated to the song are removed prior to the deletion of the song.
|
||||||
are removed prior to the deletion of the song.
|
|
||||||
|
|
||||||
:param song_id: The ID of the song to delete.
|
:param song_id: The ID of the song to delete.
|
||||||
:param song_plugin: The song plugin instance.
|
:param song_plugin: The song plugin instance.
|
||||||
"""
|
"""
|
||||||
|
save_path = ''
|
||||||
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
|
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
|
||||||
for media_file in media_files:
|
for media_file in media_files:
|
||||||
try:
|
try:
|
||||||
os.remove(media_file.file_name)
|
os.remove(media_file.file_name)
|
||||||
except:
|
except OSError:
|
||||||
log.exception('Could not remove file: %s', media_file.file_name)
|
log.exception('Could not remove file: %s', media_file.file_name)
|
||||||
try:
|
try:
|
||||||
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
|
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
|
||||||
|
208
openlp/plugins/songs/lib/songselect.py
Normal file
208
openlp/plugins/songs/lib/songselect.py
Normal 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
|
@ -38,11 +38,13 @@ import sqlite3
|
|||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
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 import Plugin, StringContent, build_icon
|
||||||
from openlp.core.lib.db import Manager
|
from openlp.core.lib.db import Manager
|
||||||
from openlp.core.lib.ui import create_action
|
from openlp.core.lib.ui import create_action
|
||||||
from openlp.core.utils.actions import ActionList
|
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 import clean_song, upgrade
|
||||||
from openlp.plugins.songs.lib.db import init_schema, Song
|
from openlp.plugins.songs.lib.db import init_schema, Song
|
||||||
from openlp.plugins.songs.lib.mediaitem import SongSearch
|
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.olpimport import OpenLPSongImport
|
||||||
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||||
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
__default_settings__ = {'songs/db type': 'sqlite',
|
__default_settings__ = {
|
||||||
'songs/last search type': SongSearch.Entire,
|
'songs/db type': 'sqlite',
|
||||||
'songs/last import type': SongFormat.OpenLyrics,
|
'songs/last search type': SongSearch.Entire,
|
||||||
'songs/update service on edit': False,
|
'songs/last import type': SongFormat.OpenLyrics,
|
||||||
'songs/search as type': False,
|
'songs/update service on edit': False,
|
||||||
'songs/add song from service': True,
|
'songs/search as type': False,
|
||||||
'songs/display songbar': True,
|
'songs/add song from service': True,
|
||||||
'songs/last directory import': '',
|
'songs/display songbar': True,
|
||||||
'songs/last directory export': ''
|
'songs/last directory import': '',
|
||||||
}
|
'songs/last directory export': '',
|
||||||
|
'songs/songselect username': '',
|
||||||
|
'songs/songselect password': '',
|
||||||
|
'songs/songselect searches': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SongsPlugin(Plugin):
|
class SongsPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
This is the number 1 plugin, if importance were placed on any plugins. This plugin enables the user to create,
|
This plugin enables the user to create, edit and display songs. Songs are divided into verses, and the verse order
|
||||||
edit and display songs. Songs are divided into verses, and the verse order can be specified. Authors, topics and
|
can be specified. Authors, topics and song books can be assigned to songs as well.
|
||||||
song books can be assigned to songs as well.
|
|
||||||
"""
|
"""
|
||||||
log.info('Song Plugin loaded')
|
log.info('Song Plugin loaded')
|
||||||
|
|
||||||
@ -83,6 +87,7 @@ class SongsPlugin(Plugin):
|
|||||||
self.weight = -10
|
self.weight = -10
|
||||||
self.icon_path = ':/plugins/plugin_songs.png'
|
self.icon_path = ':/plugins/plugin_songs.png'
|
||||||
self.icon = build_icon(self.icon_path)
|
self.icon = build_icon(self.icon_path)
|
||||||
|
self.songselect_form = None
|
||||||
|
|
||||||
def check_pre_conditions(self):
|
def check_pre_conditions(self):
|
||||||
"""
|
"""
|
||||||
@ -92,10 +97,11 @@ class SongsPlugin(Plugin):
|
|||||||
|
|
||||||
def initialise(self):
|
def initialise(self):
|
||||||
"""
|
"""
|
||||||
Lets Initialise the plugin
|
Initialise the plugin
|
||||||
"""
|
"""
|
||||||
log.info('Songs Initialising')
|
log.info('Songs Initialising')
|
||||||
super(SongsPlugin, self).initialise()
|
super(SongsPlugin, self).initialise()
|
||||||
|
self.songselect_form = SongSelectForm(Registry().get('main_window'), self, self.manager)
|
||||||
self.song_import_item.setVisible(True)
|
self.song_import_item.setVisible(True)
|
||||||
self.song_export_item.setVisible(True)
|
self.song_export_item.setVisible(True)
|
||||||
self.tools_reindex_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.'),
|
tooltip=translate('SongsPlugin', 'Import songs using the import wizard.'),
|
||||||
triggers=self.on_song_import_item_clicked)
|
triggers=self.on_song_import_item_clicked)
|
||||||
import_menu.addAction(self.song_import_item)
|
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):
|
def add_export_menu_Item(self, export_menu):
|
||||||
"""
|
"""
|
||||||
Give the Songs plugin the opportunity to add items to the **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
|
# Main song import menu item - will eventually be the only one
|
||||||
self.song_export_item = create_action(
|
self.song_export_item = create_action(
|
||||||
@ -179,29 +191,42 @@ class SongsPlugin(Plugin):
|
|||||||
"""
|
"""
|
||||||
DuplicateSongRemovalForm(self).exec_()
|
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):
|
def on_song_import_item_clicked(self):
|
||||||
"""
|
"""
|
||||||
The song import option has been selected
|
Run the song import wizard.
|
||||||
"""
|
"""
|
||||||
if self.media_item:
|
if self.media_item:
|
||||||
self.media_item.on_import_click()
|
self.media_item.on_import_click()
|
||||||
|
|
||||||
def on_song_export_item_clicked(self):
|
def on_song_export_item_clicked(self):
|
||||||
"""
|
"""
|
||||||
The song export option has been selected
|
Run the song export wizard.
|
||||||
"""
|
"""
|
||||||
if self.media_item:
|
if self.media_item:
|
||||||
self.media_item.on_export_click()
|
self.media_item.on_export_click()
|
||||||
|
|
||||||
def about(self):
|
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>'
|
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):
|
def uses_theme(self, theme):
|
||||||
"""
|
"""
|
||||||
Called to find out if the song plugin is currently using a 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):
|
if self.manager.get_all_objects(Song, Song.theme_name == theme):
|
||||||
return True
|
return True
|
||||||
|
BIN
resources/images/general_back.png
Normal file
BIN
resources/images/general_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 907 B |
BIN
resources/images/general_find.png
Normal file
BIN
resources/images/general_find.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 942 B |
@ -61,6 +61,8 @@
|
|||||||
<file>general_email.png</file>
|
<file>general_email.png</file>
|
||||||
<file>general_revert.png</file>
|
<file>general_revert.png</file>
|
||||||
<file>general_clone.png</file>
|
<file>general_clone.png</file>
|
||||||
|
<file>general_find.png</file>
|
||||||
|
<file>general_back.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="slides">
|
<qresource prefix="slides">
|
||||||
<file>slide_close.png</file>
|
<file>slide_close.png</file>
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Base directory for tests
|
||||||
|
"""
|
||||||
import sip
|
import sip
|
||||||
sip.setapi('QDate', 2)
|
sip.setapi('QDate', 2)
|
||||||
sip.setapi('QDateTime', 2)
|
sip.setapi('QDateTime', 2)
|
||||||
@ -11,9 +15,11 @@ import sys
|
|||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
if sys.version_info[1] >= 3:
|
if sys.version_info[1] >= 3:
|
||||||
from unittest.mock import MagicMock, patch, mock_open
|
from unittest.mock import MagicMock, patch, mock_open, call
|
||||||
else:
|
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.
|
# Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a QApplication.
|
||||||
application = QtGui.QApplication([])
|
application = QtGui.QApplication([])
|
||||||
|
|
||||||
|
__all__ = ['MagicMock', 'patch', 'mock_open', 'call', 'application']
|
||||||
|
@ -38,20 +38,20 @@ from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
|
|||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
'..', '..', '..', 'resources', 'songbeamersongs'))
|
'..', '..', '..', 'resources', 'songbeamersongs'))
|
||||||
SONG_TEST_DATA = {'Lobsinget dem Herrn.sng':
|
SONG_TEST_DATA = {
|
||||||
{'title': 'GL 1 - Lobsinget dem Herrn',
|
'Lobsinget dem Herrn.sng': {
|
||||||
'verses':
|
'title': 'GL 1 - Lobsinget dem Herrn',
|
||||||
[('1. Lobsinget dem Herrn,\no preiset Ihn gern!\n'
|
'verses': [
|
||||||
'Anbetung und Lob Ihm gebühret.\n', 'v'),
|
('1. Lobsinget dem Herrn,\no preiset Ihn gern!\nAnbetung und Lob Ihm gebühret.\n', 'v'),
|
||||||
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\n'
|
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\nzu sterben für unsere Sünden!\n', 'v'),
|
||||||
'zu sterben für unsere Sünden!\n', 'v'),
|
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\nEr sitzet zur Rechten des Vaters.\n', 'v'),
|
||||||
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\n'
|
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
|
||||||
'Er sitzet zur Rechten des Vaters.\n', 'v'),
|
],
|
||||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\n'
|
'song_book_name': 'Glaubenslieder I',
|
||||||
'bis Er uns zur Herrlichket führet!\n\n', 'v')],
|
'song_number': "1"
|
||||||
'song_book_name': 'Glaubenslieder I',
|
}
|
||||||
'song_number': "1"}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class TestSongBeamerImport(TestCase):
|
class TestSongBeamerImport(TestCase):
|
||||||
"""
|
"""
|
||||||
|
382
tests/functional/openlp_plugins/songs/test_songselect.py
Normal file
382
tests/functional/openlp_plugins/songs/test_songselect.py
Normal 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')
|
@ -50,10 +50,13 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
|
|||||||
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
|
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
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 that loading a SongShow Plus file works correctly on various files
|
||||||
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')))
|
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):
|
class TestSongShowPlusImport(TestCase):
|
||||||
|
31
tests/helpers/__init__.py
Normal file
31
tests/helpers/__init__.py
Normal 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.
|
||||||
|
"""
|
Loading…
Reference in New Issue
Block a user