forked from openlp/openlp
merged with head
This commit is contained in:
commit
54e80b6a03
@ -134,4 +134,4 @@ from .registrymixin import RegistryMixin
|
||||
from .uistrings import UiStrings
|
||||
from .settings import Settings
|
||||
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
|
||||
"""
|
||||
from PyQt4 import QtGui
|
||||
|
||||
|
||||
class HideMode(object):
|
||||
@ -77,6 +78,29 @@ class DisplayControllerType(object):
|
||||
Plugin = 2
|
||||
|
||||
|
||||
class SingleColumnTableWidget(QtGui.QTableWidget):
|
||||
"""
|
||||
Class to for a single column table widget to use for the verse table widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(SingleColumnTableWidget, self).__init__(parent)
|
||||
self.horizontalHeader().setVisible(False)
|
||||
self.setColumnCount(1)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""
|
||||
Resize the first column together with the widget.
|
||||
"""
|
||||
QtGui.QTableWidget.resizeEvent(self, event)
|
||||
if self.columnCount():
|
||||
self.setColumnWidth(0, event.size().width())
|
||||
self.resizeRowsToContents()
|
||||
|
||||
|
||||
from .firsttimeform import FirstTimeForm
|
||||
from .firsttimelanguageform import FirstTimeLanguageForm
|
||||
from .themelayoutform import ThemeLayoutForm
|
||||
@ -101,8 +125,8 @@ from .mediadockmanager import MediaDockManager
|
||||
from .servicemanager import ServiceManager
|
||||
from .thememanager import ThemeManager
|
||||
|
||||
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager',
|
||||
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm',
|
||||
'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm',
|
||||
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
|
||||
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController']
|
||||
__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm',
|
||||
'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm',
|
||||
'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay',
|
||||
'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm',
|
||||
'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'SingleColumnTableWidget']
|
||||
|
@ -119,8 +119,10 @@ class Ui_MainWindow(object):
|
||||
self.recent_files_menu = QtGui.QMenu(self.file_menu)
|
||||
self.recent_files_menu.setObjectName('recentFilesMenu')
|
||||
self.file_import_menu = QtGui.QMenu(self.file_menu)
|
||||
self.file_import_menu.setIcon(build_icon(u':/general/general_import.png'))
|
||||
self.file_import_menu.setObjectName('file_import_menu')
|
||||
self.file_export_menu = QtGui.QMenu(self.file_menu)
|
||||
self.file_export_menu.setIcon(build_icon(u':/general/general_export.png'))
|
||||
self.file_export_menu.setObjectName('file_export_menu')
|
||||
# View Menu
|
||||
self.view_menu = QtGui.QMenu(self.menu_bar)
|
||||
@ -230,7 +232,7 @@ class Ui_MainWindow(object):
|
||||
main_window, 'modeDefaultItem', checked=False, category=UiStrings().ViewMode, can_shortcuts=True)
|
||||
self.mode_setup_item = create_action(main_window, 'modeSetupItem', checked=False, category=UiStrings().ViewMode,
|
||||
can_shortcuts=True)
|
||||
self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode,
|
||||
self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode,
|
||||
can_shortcuts=True)
|
||||
self.mode_group = QtGui.QActionGroup(main_window)
|
||||
self.mode_group.addAction(self.mode_default_item)
|
||||
@ -239,10 +241,10 @@ class Ui_MainWindow(object):
|
||||
self.mode_default_item.setChecked(True)
|
||||
action_list.add_category(UiStrings().Tools, CategoryOrder.standard_menu)
|
||||
self.tools_add_tool_item = create_action(main_window,
|
||||
'toolsAddToolItem', icon=':/tools/tools_add.png',
|
||||
'toolsAddToolItem', icon=':/tools/tools_add.png',
|
||||
category=UiStrings().Tools, can_shortcuts=True)
|
||||
self.tools_open_data_folder = create_action(main_window,
|
||||
'toolsOpenDataFolder', icon=':/general/general_open.png',
|
||||
'toolsOpenDataFolder', icon=':/general/general_open.png',
|
||||
category=UiStrings().Tools, can_shortcuts=True)
|
||||
self.tools_first_time_wizard = create_action(main_window,
|
||||
'toolsFirstTimeWizard', icon=':/general/general_revert.png',
|
||||
@ -268,24 +270,24 @@ class Ui_MainWindow(object):
|
||||
language_item = create_action(main_window, key, checked=qm_list[key] == saved_language)
|
||||
add_actions(self.language_group, [language_item])
|
||||
self.settings_shortcuts_item = create_action(main_window, 'settingsShortcutsItem',
|
||||
icon=':/system/system_configure_shortcuts.png',
|
||||
icon=':/system/system_configure_shortcuts.png',
|
||||
category=UiStrings().Settings, can_shortcuts=True)
|
||||
# Formatting Tags were also known as display tags.
|
||||
self.formatting_tag_item = create_action(main_window, 'displayTagItem',
|
||||
icon=':/system/tag_editor.png', category=UiStrings().Settings,
|
||||
icon=':/system/tag_editor.png', category=UiStrings().Settings,
|
||||
can_shortcuts=True)
|
||||
self.settings_configure_item = create_action(main_window, 'settingsConfigureItem',
|
||||
icon=':/system/system_settings.png', can_shortcuts=True,
|
||||
icon=':/system/system_settings.png', can_shortcuts=True,
|
||||
category=UiStrings().Settings)
|
||||
# Give QT Extra Hint that this is the Preferences Menu Item
|
||||
self.settings_configure_item.setMenuRole(QtGui.QAction.PreferencesRole)
|
||||
self.settings_import_item = create_action(main_window, 'settingsImportItem',
|
||||
self.settings_import_item = create_action(main_window, 'settingsImportItem',
|
||||
category=UiStrings().Import, can_shortcuts=True)
|
||||
self.settings_export_item = create_action(main_window, 'settingsExportItem',
|
||||
self.settings_export_item = create_action(main_window, 'settingsExportItem',
|
||||
category=UiStrings().Export, can_shortcuts=True)
|
||||
action_list.add_category(UiStrings().Help, CategoryOrder.standard_menu)
|
||||
self.about_item = create_action(main_window, 'aboutItem', icon=':/system/system_about.png',
|
||||
can_shortcuts=True, category=UiStrings().Help,
|
||||
can_shortcuts=True, category=UiStrings().Help,
|
||||
triggers=self.on_about_item_clicked)
|
||||
# Give QT Extra Hint that this is an About Menu Item
|
||||
self.about_item.setMenuRole(QtGui.QAction.AboutRole)
|
||||
@ -302,13 +304,13 @@ class Ui_MainWindow(object):
|
||||
self.web_site_item = create_action(main_window, 'webSiteItem', can_shortcuts=True, category=UiStrings().Help)
|
||||
# Shortcuts not connected to buttons or menu entries.
|
||||
self.search_shortcut_action = create_action(main_window,
|
||||
'searchShortcut', can_shortcuts=True,
|
||||
'searchShortcut', can_shortcuts=True,
|
||||
category=translate('OpenLP.MainWindow', 'General'),
|
||||
triggers=self.on_search_shortcut_triggered)
|
||||
add_actions(self.file_import_menu, (self.settings_import_item, None, self.import_theme_item,
|
||||
self.import_language_item))
|
||||
add_actions(self.file_export_menu, (self.settings_export_item, None, self.export_theme_item,
|
||||
self.export_language_item))
|
||||
add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item,
|
||||
self.import_language_item, None))
|
||||
add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item,
|
||||
self.export_language_item, None))
|
||||
add_actions(self.file_menu, (self.file_new_item, self.file_open_item,
|
||||
self.file_save_item, self.file_save_as_item, self.recent_files_menu.menuAction(), None,
|
||||
self.file_import_menu.menuAction(), self.file_export_menu.menuAction(), None,
|
||||
@ -651,7 +653,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
||||
'Time Wizard?\n\nRe-running this wizard may make changes to your '
|
||||
'current OpenLP configuration and possibly add songs to your '
|
||||
'#existing songs list and change your default theme.'),
|
||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
|
||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
|
||||
QtGui.QMessageBox.No),
|
||||
QtGui.QMessageBox.No)
|
||||
if answer == QtGui.QMessageBox.No:
|
||||
|
@ -426,9 +426,9 @@ def get_locale_key(string):
|
||||
The corresponding string.
|
||||
"""
|
||||
string = string.lower()
|
||||
# For Python 3 on platforms other than Windows ICU is not necessary. In those cases locale.strxfrm(str) can be used.
|
||||
if os.name == 'nt':
|
||||
global ICU_COLLATOR
|
||||
# ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
|
||||
global ICU_COLLATOR
|
||||
try:
|
||||
if ICU_COLLATOR is None:
|
||||
import icu
|
||||
from .languagemanager import LanguageManager
|
||||
@ -436,8 +436,8 @@ def get_locale_key(string):
|
||||
icu_locale = icu.Locale(language)
|
||||
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
|
||||
return ICU_COLLATOR.getSortKey(string)
|
||||
return locale.strxfrm(string).encode()
|
||||
|
||||
except:
|
||||
return locale.strxfrm(string).encode()
|
||||
|
||||
def get_natural_key(string):
|
||||
"""
|
||||
|
@ -32,6 +32,7 @@ from PyQt4 import QtCore, QtGui
|
||||
from openlp.core.common import UiStrings, translate
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.ui import create_button_box, create_button
|
||||
from openlp.core.ui import SingleColumnTableWidget
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
|
||||
|
||||
@ -346,25 +347,3 @@ def create_combo_box(parent, name):
|
||||
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
|
||||
combo_box.setObjectName(name)
|
||||
return combo_box
|
||||
|
||||
|
||||
class SingleColumnTableWidget(QtGui.QTableWidget):
|
||||
"""
|
||||
Class to for a single column table widget to use for the verse table widget.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(SingleColumnTableWidget, self).__init__(parent)
|
||||
self.horizontalHeader().setVisible(False)
|
||||
self.setColumnCount(1)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""
|
||||
Resize the first column together with the widget.
|
||||
"""
|
||||
QtGui.QTableWidget.resizeEvent(self, event)
|
||||
if self.columnCount():
|
||||
self.setColumnWidth(0, event.size().width())
|
||||
self.resizeRowsToContents()
|
||||
|
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
|
||||
# { or } - marking the beginning/end of a group
|
||||
# a run of characters without any \ { } or end-of-line
|
||||
PATTERN = re.compile(r"(\\\*)?\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z*])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
|
||||
PATTERN = re.compile(
|
||||
r"(\\\*)?\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z*])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
|
||||
# RTF control words which specify a "destination" to be ignored.
|
||||
DESTINATIONS = frozenset((
|
||||
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor',
|
||||
'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref', 'atntime',
|
||||
'atrfend', 'atrfstart', 'author', 'background', 'bkmkend',
|
||||
'bkmkstart', 'blipuid', 'buptim', 'category',
|
||||
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim',
|
||||
'datafield', 'datastore', 'defchp', 'defpap', 'do', 'doccomm',
|
||||
'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname',
|
||||
'falt', 'fchars', 'ffdeftext', 'ffentrymcr', 'ffexitmcr',
|
||||
'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext',
|
||||
'file', 'filetbl', 'fldinst', 'fldtype', 'fname',
|
||||
'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr',
|
||||
'footnote', 'formfield', 'ftncn', 'ftnsep', 'ftnsepc', 'g',
|
||||
'generator', 'gridtbl', 'header', 'headerf', 'headerl',
|
||||
'headerr', 'hl', 'hlfr', 'hlinkbase', 'hlloc', 'hlsrc', 'hsv',
|
||||
'htmltag', 'info', 'keycode', 'keywords', 'latentstyles',
|
||||
'lchars', 'levelnumbers', 'leveltext', 'lfolevel', 'linkval',
|
||||
'list', 'listlevel', 'listname', 'listoverride',
|
||||
'listoverridetable', 'listpicture', 'liststylename', 'listtable',
|
||||
'listtext', 'lsdlockedexcept', 'macc', 'maccPr', 'mailmerge',
|
||||
'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr',
|
||||
'mbaseJc', 'mbegChr', 'mborderBox', 'mborderBoxPr', 'mbox',
|
||||
'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide',
|
||||
'mden', 'mdiff', 'mdPr', 'me', 'mendChr', 'meqArr', 'meqArrPr',
|
||||
'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr',
|
||||
'mgroupChrPr', 'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight',
|
||||
'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow',
|
||||
'mlimlowPr', 'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname',
|
||||
'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
|
||||
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs',
|
||||
'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
|
||||
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname',
|
||||
'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
|
||||
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag',
|
||||
'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr', 'mnoBreak',
|
||||
'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr',
|
||||
'mopEmu', 'mphant', 'mphantPr', 'mplcHide', 'mpos', 'mr',
|
||||
'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre',
|
||||
'msPrePr', 'msSub', 'msSubPr', 'msSubSup', 'msSubSupPr', 'msSup',
|
||||
'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV',
|
||||
'msub', 'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype',
|
||||
'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
|
||||
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile',
|
||||
'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
|
||||
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops',
|
||||
'oldsprops', 'oldtprops', 'oleclsid', 'operator', 'panose',
|
||||
'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict',
|
||||
'pn', 'pnseclvl', 'pntext', 'pntxta', 'pntxtb', 'printim',
|
||||
'private', 'propname', 'protend', 'protstart', 'protusertbl',
|
||||
'pxe', 'result', 'revtbl', 'revtim', 'rsidtbl', 'rxe', 'shp',
|
||||
'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp',
|
||||
'staticval', 'stylesheet', 'subject', 'sv', 'svb', 'tc',
|
||||
'template', 'themedata', 'title', 'txe', 'ud', 'upr',
|
||||
'userprops', 'wgrffmtfilter', 'windowcaption', 'writereservation',
|
||||
'writereservhash', 'xe', 'xform', 'xmlattrname', 'xmlattrvalue',
|
||||
'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'))
|
||||
'aftncn', 'aftnsep', 'aftnsepc', 'annotation', 'atnauthor', 'atndate', 'atnicn', 'atnid', 'atnparent', 'atnref',
|
||||
'atntime', 'atrfend', 'atrfstart', 'author', 'background', 'bkmkend', 'bkmkstart', 'blipuid', 'buptim', 'category',
|
||||
'colorschememapping', 'colortbl', 'comment', 'company', 'creatim', 'datafield', 'datastore', 'defchp', 'defpap',
|
||||
'do', 'doccomm', 'docvar', 'dptxbxtext', 'ebcend', 'ebcstart', 'factoidname', 'falt', 'fchars', 'ffdeftext',
|
||||
'ffentrymcr', 'ffexitmcr', 'ffformat', 'ffhelptext', 'ffl', 'ffname', 'ffstattext', 'file', 'filetbl', 'fldinst',
|
||||
'fldtype', 'fname', 'fontemb', 'fontfile', 'footer', 'footerf', 'footerl', 'footerr', 'footnote', 'formfield',
|
||||
'ftncn', 'ftnsep', 'ftnsepc', 'g', 'generator', 'gridtbl', 'header', 'headerf', 'headerl', 'headerr', 'hl', 'hlfr',
|
||||
'hlinkbase', 'hlloc', 'hlsrc', 'hsv', 'htmltag', 'info', 'keycode', 'keywords', 'latentstyles', 'lchars',
|
||||
'levelnumbers', 'leveltext', 'lfolevel', 'linkval', 'list', 'listlevel', 'listname', 'listoverride',
|
||||
'listoverridetable', 'listpicture', 'liststylename', 'listtable', 'listtext', 'lsdlockedexcept', 'macc', 'maccPr',
|
||||
'mailmerge', 'maln', 'malnScr', 'manager', 'margPr', 'mbar', 'mbarPr', 'mbaseJc', 'mbegChr', 'mborderBox',
|
||||
'mborderBoxPr', 'mbox', 'mboxPr', 'mchr', 'mcount', 'mctrlPr', 'md', 'mdeg', 'mdegHide', 'mden', 'mdiff', 'mdPr',
|
||||
'me', 'mendChr', 'meqArr', 'meqArrPr', 'mf', 'mfName', 'mfPr', 'mfunc', 'mfuncPr', 'mgroupChr', 'mgroupChrPr',
|
||||
'mgrow', 'mhideBot', 'mhideLeft', 'mhideRight', 'mhideTop', 'mhtmltag', 'mlim', 'mlimloc', 'mlimlow', 'mlimlowPr',
|
||||
'mlimupp', 'mlimuppPr', 'mm', 'mmaddfieldname', 'mmath', 'mmathPict', 'mmathPr', 'mmaxdist', 'mmc', 'mmcJc',
|
||||
'mmconnectstr', 'mmconnectstrdata', 'mmcPr', 'mmcs', 'mmdatasource', 'mmheadersource', 'mmmailsubject', 'mmodso',
|
||||
'mmodsofilter', 'mmodsofldmpdata', 'mmodsomappedname', 'mmodsoname', 'mmodsorecipdata', 'mmodsosort', 'mmodsosrc',
|
||||
'mmodsotable', 'mmodsoudl', 'mmodsoudldata', 'mmodsouniquetag', 'mmPr', 'mmquery', 'mmr', 'mnary', 'mnaryPr',
|
||||
'mnoBreak', 'mnum', 'mobjDist', 'moMath', 'moMathPara', 'moMathParaPr', 'mopEmu', 'mphant', 'mphantPr', 'mplcHide',
|
||||
'mpos', 'mr', 'mrad', 'mradPr', 'mrPr', 'msepChr', 'mshow', 'mshp', 'msPre', 'msPrePr', 'msSub', 'msSubPr',
|
||||
'msSubSup', 'msSubSupPr', 'msSup', 'msSupPr', 'mstrikeBLTR', 'mstrikeH', 'mstrikeTLBR', 'mstrikeV', 'msub',
|
||||
'msubHide', 'msup', 'msupHide', 'mtransp', 'mtype', 'mvertJc', 'mvfmf', 'mvfml', 'mvtof', 'mvtol', 'mzeroAsc',
|
||||
'mzFrodesc', 'mzeroWid', 'nesttableprops', 'nextfile', 'nonesttables', 'objalias', 'objclass', 'objdata', 'object',
|
||||
'objname', 'objsect', 'objtime', 'oldcprops', 'oldpprops', 'oldsprops', 'oldtprops', 'oleclsid', 'operator',
|
||||
'panose', 'password', 'passwordhash', 'pgp', 'pgptbl', 'picprop', 'pict', 'pn', 'pnseclvl', 'pntext', 'pntxta',
|
||||
'pntxtb', 'printim', 'private', 'propname', 'protend', 'protstart', 'protusertbl', 'pxe', 'result', 'revtbl',
|
||||
'revtim', 'rsidtbl', 'rxe', 'shp', 'shpgrp', 'shpinst', 'shppict', 'shprslt', 'shptxt', 'sn', 'sp', 'staticval',
|
||||
'stylesheet', 'subject', 'sv', 'svb', 'tc', 'template', 'themedata', 'title', 'txe', 'ud', 'upr', 'userprops',
|
||||
'wgrffmtfilter', 'windowcaption', 'writereservation', 'writereservhash', 'xe', 'xform', 'xmlattrname',
|
||||
'xmlattrvalue', 'xmlclose', 'xmlname', 'xmlnstbl', 'xmlopen'
|
||||
))
|
||||
# Translation of some special characters.
|
||||
SPECIAL_CHARS = {
|
||||
'\n': '\n',
|
||||
@ -142,7 +119,8 @@ SPECIAL_CHARS = {
|
||||
'ltrmark': '\u200E',
|
||||
'rtlmark': '\u200F',
|
||||
'zwj': '\u200D',
|
||||
'zwnj': '\u200C'}
|
||||
'zwnj': '\u200C'
|
||||
}
|
||||
CHARSET_MAPPING = {
|
||||
'0': 'cp1252',
|
||||
'128': 'cp932',
|
||||
@ -156,7 +134,8 @@ CHARSET_MAPPING = {
|
||||
'186': 'cp1257',
|
||||
'204': 'cp1251',
|
||||
'222': 'cp874',
|
||||
'238': 'cp1250'}
|
||||
'238': 'cp1250'
|
||||
}
|
||||
|
||||
|
||||
class VerseType(object):
|
||||
@ -171,14 +150,7 @@ class VerseType(object):
|
||||
Ending = 5
|
||||
Other = 6
|
||||
|
||||
names = [
|
||||
'Verse',
|
||||
'Chorus',
|
||||
'Bridge',
|
||||
'Pre-Chorus',
|
||||
'Intro',
|
||||
'Ending',
|
||||
'Other']
|
||||
names = ['Verse', 'Chorus', 'Bridge', 'Pre-Chorus', 'Intro', 'Ending', 'Other']
|
||||
tags = [name[0].lower() for name in names]
|
||||
|
||||
translated_names = [
|
||||
@ -197,11 +169,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the translated UPPERCASE tag for a given tag, used to show translated verse tags in UI
|
||||
|
||||
``verse_tag``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_tag: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: A translated UPPERCASE tag
|
||||
"""
|
||||
verse_tag = verse_tag[0].lower()
|
||||
for num, tag in enumerate(VerseType.tags):
|
||||
@ -217,11 +187,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the translated name for a given tag
|
||||
|
||||
``verse_tag``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_tag: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: Translated name for the given tag
|
||||
"""
|
||||
verse_tag = verse_tag[0].lower()
|
||||
for num, tag in enumerate(VerseType.tags):
|
||||
@ -237,11 +205,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the VerseType for a given tag
|
||||
|
||||
``verse_tag``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_tag: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: A VerseType of the tag
|
||||
"""
|
||||
verse_tag = verse_tag[0].lower()
|
||||
for num, tag in enumerate(VerseType.tags):
|
||||
@ -257,11 +223,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the VerseType for a given tag
|
||||
|
||||
``verse_tag``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_tag: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: The VerseType of a translated tag
|
||||
"""
|
||||
verse_tag = verse_tag[0].lower()
|
||||
for num, tag in enumerate(VerseType.translated_tags):
|
||||
@ -277,11 +241,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the VerseType for a given string
|
||||
|
||||
``verse_name``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_name: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: The VerseType determined from the string
|
||||
"""
|
||||
verse_name = verse_name.lower()
|
||||
for num, name in enumerate(VerseType.names):
|
||||
@ -294,8 +256,8 @@ class VerseType(object):
|
||||
"""
|
||||
Return the VerseType for a given string
|
||||
|
||||
``verse_name``
|
||||
The string to return a VerseType for
|
||||
:param verse_name: The string to return a VerseType for
|
||||
:return: A VerseType
|
||||
"""
|
||||
verse_name = verse_name.lower()
|
||||
for num, translation in enumerate(VerseType.translated_names):
|
||||
@ -307,11 +269,9 @@ class VerseType(object):
|
||||
"""
|
||||
Return the VerseType for a given string
|
||||
|
||||
``verse_name``
|
||||
The string to return a VerseType for
|
||||
|
||||
``default``
|
||||
Default return value if no matching tag is found
|
||||
:param verse_name: The string to return a VerseType for
|
||||
:param default: Default return value if no matching tag is found
|
||||
:return: A VerseType
|
||||
"""
|
||||
if len(verse_name) > 1:
|
||||
verse_index = VerseType.from_translated_string(verse_name)
|
||||
@ -331,22 +291,21 @@ def retrieve_windows_encoding(recommendation=None):
|
||||
Determines which encoding to use on an information source. The process uses both automated detection, which is
|
||||
passed to this method as a recommendation, and user confirmation to return an encoding.
|
||||
|
||||
``recommendation``
|
||||
A recommended encoding discovered programmatically for the user to confirm.
|
||||
:param recommendation: A recommended encoding discovered programmatically for the user to confirm.
|
||||
:return: A list of recommended encodings, or None
|
||||
"""
|
||||
# map chardet result to compatible windows standard code page
|
||||
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874',
|
||||
'SHIFT_JIS': 'cp932', 'GB2312': 'cp936', 'HZ-GB-2312': 'cp936',
|
||||
'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
|
||||
'windows-1250': 'cp1250', 'windows-1251': 'cp1251',
|
||||
'windows-1252': 'cp1252', 'ISO-8859-7': 'cp1253',
|
||||
'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
|
||||
'windows-1255': 'cp1255'}
|
||||
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874', 'SHIFT_JIS': 'cp932', 'GB2312': 'cp936',
|
||||
'HZ-GB-2312': 'cp936', 'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
|
||||
'windows-1250': 'cp1250', 'windows-1251': 'cp1251', 'windows-1252': 'cp1252',
|
||||
'ISO-8859-7': 'cp1253', 'windows-1253': 'cp1253', 'ISO-8859-8': 'cp1255',
|
||||
'windows-1255': 'cp1255'}
|
||||
if recommendation in codepage_mapping:
|
||||
recommendation = codepage_mapping[recommendation]
|
||||
|
||||
# Show dialog for encoding selection
|
||||
encodings = [('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
|
||||
encodings = [
|
||||
('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
|
||||
('cp1257', translate('SongsPlugin', 'Baltic (CP-1257)')),
|
||||
('cp1250', translate('SongsPlugin', 'Central European (CP-1250)')),
|
||||
('cp1251', translate('SongsPlugin', 'Cyrillic (CP-1251)')),
|
||||
@ -359,7 +318,8 @@ def retrieve_windows_encoding(recommendation=None):
|
||||
('cp950', translate('SongsPlugin', 'Traditional Chinese (CP-950)')),
|
||||
('cp1254', translate('SongsPlugin', 'Turkish (CP-1254)')),
|
||||
('cp1258', translate('SongsPlugin', 'Vietnam (CP-1258)')),
|
||||
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))]
|
||||
('cp1252', translate('SongsPlugin', 'Western European (CP-1252)'))
|
||||
]
|
||||
recommended_index = -1
|
||||
if recommendation:
|
||||
for index in range(len(encodings)):
|
||||
@ -367,17 +327,20 @@ def retrieve_windows_encoding(recommendation=None):
|
||||
recommended_index = index
|
||||
break
|
||||
if recommended_index > -1:
|
||||
choice = QtGui.QInputDialog.getItem(None,
|
||||
choice = QtGui.QInputDialog.getItem(
|
||||
None,
|
||||
translate('SongsPlugin', 'Character Encoding'),
|
||||
translate('SongsPlugin', 'The codepage setting is responsible\n'
|
||||
'for the correct character representation.\nUsually you are fine with the preselected choice.'),
|
||||
'for the correct character representation.\n'
|
||||
'Usually you are fine with the preselected choice.'),
|
||||
[pair[1] for pair in encodings], recommended_index, False)
|
||||
else:
|
||||
choice = QtGui.QInputDialog.getItem(None,
|
||||
choice = QtGui.QInputDialog.getItem(
|
||||
None,
|
||||
translate('SongsPlugin', 'Character Encoding'),
|
||||
translate('SongsPlugin', 'Please choose the character encoding.\n'
|
||||
'The encoding is responsible for the correct character representation.'),
|
||||
[pair[1] for pair in encodings], 0, False)
|
||||
'The encoding is responsible for the correct character representation.'),
|
||||
[pair[1] for pair in encodings], 0, False)
|
||||
if not choice[1]:
|
||||
return None
|
||||
return next(filter(lambda item: item[1] == choice[0], encodings))[0]
|
||||
@ -386,6 +349,9 @@ def retrieve_windows_encoding(recommendation=None):
|
||||
def clean_string(string):
|
||||
"""
|
||||
Strips punctuation from the passed string to assist searching.
|
||||
|
||||
:param string: The string to clean
|
||||
:return: A clean string
|
||||
"""
|
||||
return WHITESPACE.sub(' ', APOSTROPHE.sub('', string)).lower()
|
||||
|
||||
@ -393,6 +359,9 @@ def clean_string(string):
|
||||
def clean_title(title):
|
||||
"""
|
||||
Cleans the song title by removing Unicode control chars groups C0 & C1, as well as any trailing spaces.
|
||||
|
||||
:param title: The song title to clean
|
||||
:return: A clean title
|
||||
"""
|
||||
return CONTROL_CHARS.sub('', title).rstrip()
|
||||
|
||||
@ -402,11 +371,8 @@ def clean_song(manager, song):
|
||||
Cleans the search title, rebuilds the search lyrics, adds a default author if the song does not have one and other
|
||||
clean ups. This should always called when a new song is added or changed.
|
||||
|
||||
``manager``
|
||||
The song's manager.
|
||||
|
||||
``song``
|
||||
The song object.
|
||||
:param manager: The song database manager object.
|
||||
:param song: The song object.
|
||||
"""
|
||||
from .xml import SongXML
|
||||
|
||||
@ -419,55 +385,10 @@ def clean_song(manager, song):
|
||||
else:
|
||||
song.alternate_title = ''
|
||||
song.search_title = clean_string(song.title) + '@' + clean_string(song.alternate_title)
|
||||
# Only do this, if we the song is a 1.9.4 song (or older).
|
||||
if song.lyrics.find('<lyrics language="en">') != -1:
|
||||
# Remove the old "language" attribute from lyrics tag (prior to 1.9.5). This is not very important, but this
|
||||
# keeps the database clean. This can be removed when everybody has cleaned his songs.
|
||||
song.lyrics = song.lyrics.replace('<lyrics language="en">', '<lyrics>')
|
||||
verses = SongXML().get_verses(song.lyrics)
|
||||
song.search_lyrics = ' '.join([clean_string(verse[1])
|
||||
for verse in verses])
|
||||
# We need a new and clean SongXML instance.
|
||||
sxml = SongXML()
|
||||
# Rebuild the song's verses, to remove any wrong verse names (for example translated ones), which might have
|
||||
# been added prior to 1.9.5.
|
||||
# List for later comparison.
|
||||
compare_order = []
|
||||
for verse in verses:
|
||||
verse_type = VerseType.tags[VerseType.from_loose_input(verse[0]['type'])]
|
||||
sxml.add_verse_to_lyrics(
|
||||
verse_type,
|
||||
verse[0]['label'],
|
||||
verse[1],
|
||||
verse[0].get('lang')
|
||||
)
|
||||
compare_order.append(('%s%s' % (verse_type, verse[0]['label'])).upper())
|
||||
if verse[0]['label'] == '1':
|
||||
compare_order.append(verse_type.upper())
|
||||
song.lyrics = str(sxml.extract_xml(), 'utf-8')
|
||||
# Rebuild the verse order, to convert translated verse tags, which might have been added prior to 1.9.5.
|
||||
if song.verse_order:
|
||||
order = CONTROL_CHARS.sub('', song.verse_order).strip().split()
|
||||
else:
|
||||
order = []
|
||||
new_order = []
|
||||
for verse_def in order:
|
||||
verse_type = VerseType.tags[
|
||||
VerseType.from_loose_input(verse_def[0])]
|
||||
if len(verse_def) > 1:
|
||||
new_order.append(('%s%s' % (verse_type, verse_def[1:])).upper())
|
||||
else:
|
||||
new_order.append(verse_type.upper())
|
||||
song.verse_order = ' '.join(new_order)
|
||||
# Check if the verse order contains tags for verses which do not exist.
|
||||
for order in new_order:
|
||||
if order not in compare_order:
|
||||
song.verse_order = ''
|
||||
break
|
||||
else:
|
||||
verses = SongXML().get_verses(song.lyrics)
|
||||
song.search_lyrics = ' '.join([clean_string(verse[1])
|
||||
for verse in verses])
|
||||
if isinstance(song.lyrics, bytes):
|
||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||
verses = SongXML().get_verses(song.lyrics)
|
||||
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
|
||||
|
||||
# The song does not have any author, add one.
|
||||
if not song.authors:
|
||||
@ -484,17 +405,10 @@ def get_encoding(font, font_table, default_encoding, failed=False):
|
||||
"""
|
||||
Finds an encoding to use. Asks user, if necessary.
|
||||
|
||||
``font``
|
||||
The number of currently active font.
|
||||
|
||||
``font_table``
|
||||
Dictionary of fonts and respective encodings.
|
||||
|
||||
``default_encoding``
|
||||
The default encoding to use when font_table is empty or no font is used.
|
||||
|
||||
``failed``
|
||||
A boolean indicating whether the previous encoding didn't work.
|
||||
:param font: The number of currently active font.
|
||||
:param font_table: Dictionary of fonts and respective encodings.
|
||||
:param default_encoding: The default encoding to use when font_table is empty or no font is used.
|
||||
:param failed: A boolean indicating whether the previous encoding didn't work.
|
||||
"""
|
||||
encoding = None
|
||||
if font in font_table:
|
||||
@ -512,14 +426,11 @@ def strip_rtf(text, default_encoding=None):
|
||||
"""
|
||||
This function strips RTF control structures and returns an unicode string.
|
||||
|
||||
Thanks to Markus Jarderot (MizardX) for this code, used by permission.
|
||||
http://stackoverflow.com/questions/188545
|
||||
Thanks to Markus Jarderot (MizardX) for this code, used by permission. http://stackoverflow.com/questions/188545
|
||||
|
||||
``text``
|
||||
RTF-encoded text, a string.
|
||||
|
||||
``default_encoding``
|
||||
Default encoding to use when no encoding is specified.
|
||||
:param text: RTF-encoded text, a string.
|
||||
:param default_encoding: Default encoding to use when no encoding is specified.
|
||||
:return: A tuple ``(text, encoding)`` where ``text`` is the clean text and ``encoding`` is the detected encoding
|
||||
"""
|
||||
# Current font is the font tag we last met.
|
||||
font = ''
|
||||
@ -620,20 +531,17 @@ def strip_rtf(text, default_encoding=None):
|
||||
|
||||
def delete_song(song_id, song_plugin):
|
||||
"""
|
||||
Deletes a song from the database. Media files associated to the song
|
||||
are removed prior to the deletion of the song.
|
||||
Deletes a song from the database. Media files associated to the song are removed prior to the deletion of the song.
|
||||
|
||||
``song_id``
|
||||
The ID of the song to delete.
|
||||
|
||||
``song_plugin``
|
||||
The song plugin instance.
|
||||
:param song_id: The ID of the song to delete.
|
||||
:param song_plugin: The song plugin instance.
|
||||
"""
|
||||
save_path = ''
|
||||
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
|
||||
for media_file in media_files:
|
||||
try:
|
||||
os.remove(media_file.file_name)
|
||||
except:
|
||||
except OSError:
|
||||
log.exception('Could not remove file: %s', media_file.file_name)
|
||||
try:
|
||||
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
|
||||
|
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 openlp.core.common import UiStrings, translate
|
||||
from openlp.core.common import UiStrings, Registry, translate
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
||||
from openlp.plugins.songs.forms.songselectform import SongSelectForm
|
||||
from openlp.plugins.songs.lib import clean_song, upgrade
|
||||
from openlp.plugins.songs.lib.db import init_schema, Song
|
||||
from openlp.plugins.songs.lib.mediaitem import SongSearch
|
||||
@ -50,27 +52,29 @@ from openlp.plugins.songs.lib.importer import SongFormat
|
||||
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
|
||||
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__default_settings__ = {'songs/db type': 'sqlite',
|
||||
'songs/last search type': SongSearch.Entire,
|
||||
'songs/last import type': SongFormat.OpenLyrics,
|
||||
'songs/update service on edit': False,
|
||||
'songs/search as type': False,
|
||||
'songs/add song from service': True,
|
||||
'songs/display songbar': True,
|
||||
'songs/last directory import': '',
|
||||
'songs/last directory export': ''
|
||||
}
|
||||
__default_settings__ = {
|
||||
'songs/db type': 'sqlite',
|
||||
'songs/last search type': SongSearch.Entire,
|
||||
'songs/last import type': SongFormat.OpenLyrics,
|
||||
'songs/update service on edit': False,
|
||||
'songs/search as type': False,
|
||||
'songs/add song from service': True,
|
||||
'songs/display songbar': True,
|
||||
'songs/last directory import': '',
|
||||
'songs/last directory export': '',
|
||||
'songs/songselect username': '',
|
||||
'songs/songselect password': '',
|
||||
'songs/songselect searches': ''
|
||||
}
|
||||
|
||||
|
||||
class SongsPlugin(Plugin):
|
||||
"""
|
||||
This is the number 1 plugin, if importance were placed on any plugins. This plugin enables the user to create,
|
||||
edit and display songs. Songs are divided into verses, and the verse order can be specified. Authors, topics and
|
||||
song books can be assigned to songs as well.
|
||||
This plugin enables the user to create, edit and display songs. Songs are divided into verses, and the verse order
|
||||
can be specified. Authors, topics and song books can be assigned to songs as well.
|
||||
"""
|
||||
log.info('Song Plugin loaded')
|
||||
|
||||
@ -83,6 +87,7 @@ class SongsPlugin(Plugin):
|
||||
self.weight = -10
|
||||
self.icon_path = ':/plugins/plugin_songs.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.songselect_form = None
|
||||
|
||||
def check_pre_conditions(self):
|
||||
"""
|
||||
@ -92,10 +97,11 @@ class SongsPlugin(Plugin):
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Lets Initialise the plugin
|
||||
Initialise the plugin
|
||||
"""
|
||||
log.info('Songs Initialising')
|
||||
super(SongsPlugin, self).initialise()
|
||||
self.songselect_form = SongSelectForm(Registry().get('main_window'), self, self.manager)
|
||||
self.song_import_item.setVisible(True)
|
||||
self.song_export_item.setVisible(True)
|
||||
self.tools_reindex_item.setVisible(True)
|
||||
@ -119,12 +125,18 @@ class SongsPlugin(Plugin):
|
||||
tooltip=translate('SongsPlugin', 'Import songs using the import wizard.'),
|
||||
triggers=self.on_song_import_item_clicked)
|
||||
import_menu.addAction(self.song_import_item)
|
||||
self.import_songselect_item = create_action(
|
||||
import_menu, 'import_songselect_item', text=translate('SongsPlugin', 'CCLI SongSelect'),
|
||||
statustip=translate('SongsPlugin', 'Import songs from CCLI\'s SongSelect service.'),
|
||||
triggers=self.on_import_songselect_item_triggered
|
||||
)
|
||||
import_menu.addAction(self.import_songselect_item)
|
||||
|
||||
def add_export_menu_Item(self, export_menu):
|
||||
"""
|
||||
Give the Songs plugin the opportunity to add items to the **Export** menu.
|
||||
|
||||
:param export_menu: The actual **Export** menu item, so that your actions can use it as their parent.
|
||||
:param export_menu: The actual **Export** menu item, so that your actions can use it as their parent.
|
||||
"""
|
||||
# Main song import menu item - will eventually be the only one
|
||||
self.song_export_item = create_action(
|
||||
@ -179,29 +191,42 @@ class SongsPlugin(Plugin):
|
||||
"""
|
||||
DuplicateSongRemovalForm(self).exec_()
|
||||
|
||||
def on_import_songselect_item_triggered(self):
|
||||
"""
|
||||
Run the SongSelect importer.
|
||||
"""
|
||||
self.songselect_form.exec_()
|
||||
self.media_item.on_search_text_button_clicked()
|
||||
|
||||
def on_song_import_item_clicked(self):
|
||||
"""
|
||||
The song import option has been selected
|
||||
Run the song import wizard.
|
||||
"""
|
||||
if self.media_item:
|
||||
self.media_item.on_import_click()
|
||||
|
||||
def on_song_export_item_clicked(self):
|
||||
"""
|
||||
The song export option has been selected
|
||||
Run the song export wizard.
|
||||
"""
|
||||
if self.media_item:
|
||||
self.media_item.on_export_click()
|
||||
|
||||
def about(self):
|
||||
"""
|
||||
Provides information for the plugin manager to display.
|
||||
|
||||
:return: A translatable string with some basic information about the Songs plugin
|
||||
"""
|
||||
return translate('SongsPlugin', '<strong>Songs Plugin</strong>'
|
||||
'<br />The songs plugin provides the ability to display and manage songs.')
|
||||
'<br />The songs plugin provides the ability to display and manage songs.')
|
||||
|
||||
def uses_theme(self, theme):
|
||||
"""
|
||||
Called to find out if the song plugin is currently using a theme.
|
||||
|
||||
Returns True if the theme is being used, otherwise returns False.
|
||||
:param theme: The theme to check for usage
|
||||
:return: True if the theme is being used, otherwise returns False
|
||||
"""
|
||||
if self.manager.get_all_objects(Song, Song.theme_name == theme):
|
||||
return True
|
||||
|
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_revert.png</file>
|
||||
<file>general_clone.png</file>
|
||||
<file>general_find.png</file>
|
||||
<file>general_back.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="slides">
|
||||
<file>slide_close.png</file>
|
||||
|
@ -1,3 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Base directory for tests
|
||||
"""
|
||||
import sip
|
||||
sip.setapi('QDate', 2)
|
||||
sip.setapi('QDateTime', 2)
|
||||
@ -11,9 +15,11 @@ import sys
|
||||
from PyQt4 import QtGui
|
||||
|
||||
if sys.version_info[1] >= 3:
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
from unittest.mock import MagicMock, patch, mock_open, call
|
||||
else:
|
||||
from mock import MagicMock, patch, mock_open
|
||||
from mock import MagicMock, patch, mock_open, call
|
||||
|
||||
# Only one QApplication can be created. Use QtGui.QApplication.instance() when you need to "create" a QApplication.
|
||||
application = QtGui.QApplication([])
|
||||
|
||||
__all__ = ['MagicMock', 'patch', 'mock_open', 'call', 'application']
|
||||
|
@ -46,6 +46,7 @@ class TestSettings(TestCase):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
@ -55,7 +56,6 @@ class TestSettings(TestCase):
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.application
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
def settings_basic_test(self):
|
||||
|
@ -49,6 +49,7 @@ class TestActionList(TestCase):
|
||||
Prepare the tests
|
||||
"""
|
||||
self.action_list = ActionList.get_instance()
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
self.settings = Settings()
|
||||
fd, self.ini_file = mkstemp('.ini')
|
||||
self.settings.set_filename(self.ini_file)
|
||||
@ -59,7 +60,7 @@ class TestActionList(TestCase):
|
||||
Clean up
|
||||
"""
|
||||
self.settings.endGroup()
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
def test_add_action_same_parent(self):
|
||||
"""
|
||||
|
@ -55,6 +55,7 @@ class TestPdfController(TestCase):
|
||||
"""
|
||||
Set up the components need for all tests.
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
self.fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
@ -69,7 +70,7 @@ class TestPdfController(TestCase):
|
||||
"""
|
||||
del self.application
|
||||
try:
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
shutil.rmtree(self.thumbnail_folder)
|
||||
shutil.rmtree(self.temp_folder)
|
||||
except OSError:
|
||||
|
@ -62,6 +62,7 @@ class TestRemoteTab(TestCase):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
self.fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
@ -77,7 +78,7 @@ class TestRemoteTab(TestCase):
|
||||
del self.parent
|
||||
del self.form
|
||||
os.close(self.fd)
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
def get_ip_address_default_test(self):
|
||||
"""
|
||||
|
@ -61,6 +61,7 @@ class TestRouter(TestCase):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
self.fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
@ -73,7 +74,7 @@ class TestRouter(TestCase):
|
||||
"""
|
||||
del self.application
|
||||
os.close(self.fd)
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
def password_encrypter_test(self):
|
||||
"""
|
||||
|
@ -28,6 +28,7 @@ class TestMediaItem(TestCase):
|
||||
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
|
||||
self.media_item = SongMediaItem(None, MagicMock())
|
||||
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
@ -40,7 +41,6 @@ class TestMediaItem(TestCase):
|
||||
del self.application
|
||||
# Not all tests use settings!
|
||||
try:
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -38,20 +38,20 @@ from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..', '..', 'resources', 'songbeamersongs'))
|
||||
SONG_TEST_DATA = {'Lobsinget dem Herrn.sng':
|
||||
{'title': 'GL 1 - Lobsinget dem Herrn',
|
||||
'verses':
|
||||
[('1. Lobsinget dem Herrn,\no preiset Ihn gern!\n'
|
||||
'Anbetung und Lob Ihm gebühret.\n', 'v'),
|
||||
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\n'
|
||||
'zu sterben für unsere Sünden!\n', 'v'),
|
||||
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\n'
|
||||
'Er sitzet zur Rechten des Vaters.\n', 'v'),
|
||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\n'
|
||||
'bis Er uns zur Herrlichket führet!\n\n', 'v')],
|
||||
'song_book_name': 'Glaubenslieder I',
|
||||
'song_number': "1"}
|
||||
}
|
||||
SONG_TEST_DATA = {
|
||||
'Lobsinget dem Herrn.sng': {
|
||||
'title': 'GL 1 - Lobsinget dem Herrn',
|
||||
'verses': [
|
||||
('1. Lobsinget dem Herrn,\no preiset Ihn gern!\nAnbetung und Lob Ihm gebühret.\n', 'v'),
|
||||
('2. Lobsingt Seiner Lieb´,\ndie einzig ihn trieb,\nzu sterben für unsere Sünden!\n', 'v'),
|
||||
('3. Lobsingt Seiner Macht!\nSein Werk ist vollbracht:\nEr sitzet zur Rechten des Vaters.\n', 'v'),
|
||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
|
||||
],
|
||||
'song_book_name': 'Glaubenslieder I',
|
||||
'song_number': "1"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSongBeamerImport(TestCase):
|
||||
"""
|
||||
|
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')
|
@ -41,6 +41,7 @@ from tests.functional import patch, MagicMock
|
||||
TEST_PATH = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'songshowplussongs'))
|
||||
|
||||
|
||||
class TestSongShowPlusFileImport(SongImportTestHelper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.importer_class_name = 'SongShowPlusImport'
|
||||
@ -48,10 +49,13 @@ class TestSongShowPlusFileImport(SongImportTestHelper):
|
||||
super(TestSongShowPlusFileImport, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_song_import(self):
|
||||
test_import = self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
test_import = self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
|
||||
"""
|
||||
Test that loading a SongShow Plus file works correctly on various files
|
||||
"""
|
||||
self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.sbsong'),
|
||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
|
||||
|
||||
|
||||
class TestSongShowPlusImport(TestCase):
|
||||
|
31
tests/helpers/__init__.py
Normal file
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.
|
||||
"""
|
@ -35,6 +35,7 @@ from unittest import TestCase
|
||||
|
||||
from tests.functional import patch, MagicMock
|
||||
|
||||
|
||||
class SongImportTestHelper(TestCase):
|
||||
"""
|
||||
This class is designed to be a helper class to reduce repition when testing the import of song files.
|
||||
|
@ -7,7 +7,7 @@ import shutil
|
||||
from tempfile import mkstemp, mkdtemp
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.lib.pluginmanager import PluginManager
|
||||
@ -23,22 +23,26 @@ class TestPluginManager(TestCase):
|
||||
"""
|
||||
Some pre-test setup required.
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
fd, self.ini_file = mkstemp('.ini')
|
||||
self.temp_dir = mkdtemp('openlp')
|
||||
Settings().set_filename(self.ini_file)
|
||||
Settings().setValue('advanced/data path', self.temp_dir)
|
||||
Registry.create()
|
||||
Registry().register('service_list', MagicMock())
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
|
||||
def tearDown(self):
|
||||
del self.app
|
||||
del self.main_window
|
||||
Settings().remove('advanced/data path')
|
||||
shutil.rmtree(self.temp_dir)
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
|
||||
def find_plugins_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui, QtTest
|
||||
from PyQt4 import QtCore, QtGui, QtTest
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.ui import filerenameform
|
||||
@ -17,7 +17,11 @@ class TestStartFileRenameForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = filerenameform.FileRenameForm()
|
||||
@ -28,7 +32,6 @@ class TestStartFileRenameForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def window_title_test(self):
|
||||
"""
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import ServiceItem
|
||||
@ -20,7 +20,11 @@ class TestListPreviewWidget(TestCase):
|
||||
Create the UI.
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
self.image = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
|
||||
self.image_manager = MagicMock()
|
||||
@ -34,7 +38,6 @@ class TestListPreviewWidget(TestCase):
|
||||
"""
|
||||
del self.preview_widget
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def initial_slide_count_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ Package to test the openlp.core.ui.mainwindow package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.ui.mainwindow import MainWindow
|
||||
@ -18,7 +18,11 @@ class TestMainWindow(TestCase):
|
||||
"""
|
||||
Registry.create()
|
||||
self.registry = Registry()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
# Mock cursor busy/normal methods.
|
||||
self.app.set_busy_cursor = MagicMock()
|
||||
self.app.set_normal_cursor = MagicMock()
|
||||
@ -42,7 +46,6 @@ class TestMainWindow(TestCase):
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def restore_current_media_manager_item_test(self):
|
||||
"""
|
||||
|
@ -19,7 +19,11 @@ class TestServiceManager(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
ScreenList.create(self.app.desktop())
|
||||
Registry().register('application', MagicMock())
|
||||
with patch('openlp.core.lib.PluginManager'):
|
||||
@ -31,7 +35,6 @@ class TestServiceManager(TestCase):
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def basic_service_manager_test(self):
|
||||
"""
|
||||
|
@ -17,7 +17,11 @@ class TestStartNoteDialog(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = servicenoteform.ServiceNoteForm()
|
||||
@ -28,7 +32,6 @@ class TestStartNoteDialog(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def basic_display_test(self):
|
||||
"""
|
||||
|
@ -31,7 +31,11 @@ class TestSettingsForm(TestCase):
|
||||
self.dummy2 = MagicMock()
|
||||
self.dummy3 = MagicMock()
|
||||
self.desktop = MagicMock()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.desktop.primaryScreen.return_value = SCREEN['primary']
|
||||
self.desktop.screenCount.return_value = SCREEN['number']
|
||||
self.desktop.screenGeometry.return_value = SCREEN['size']
|
||||
@ -44,7 +48,6 @@ class TestSettingsForm(TestCase):
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.form
|
||||
del self.app
|
||||
|
||||
def basic_cancel_test(self):
|
||||
"""
|
||||
|
@ -17,7 +17,11 @@ class TestStartTimeDialog(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = starttimeform.StartTimeForm()
|
||||
@ -28,7 +32,6 @@ class TestStartTimeDialog(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def ui_defaults_test(self):
|
||||
"""
|
||||
|
@ -33,7 +33,7 @@ import os
|
||||
from unittest import TestCase
|
||||
from tempfile import mkstemp
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.ui import ThemeManager
|
||||
@ -48,9 +48,14 @@ class TestThemeManager(TestCase):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
fd, self.ini_file = mkstemp('.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
Registry.create()
|
||||
self.theme_manager = ThemeManager()
|
||||
|
||||
@ -58,9 +63,7 @@ class TestThemeManager(TestCase):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
os.unlink(self.ini_file)
|
||||
os.unlink(Settings().fileName())
|
||||
del self.app
|
||||
|
||||
def initialise_test(self):
|
||||
"""
|
||||
|
@ -21,7 +21,11 @@ class TestEditCustomForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
media_item = MagicMock()
|
||||
@ -34,7 +38,6 @@ class TestEditCustomForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def load_themes_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ Module to test the EditCustomSlideForm.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.custom.forms.editcustomslideform import EditCustomSlideForm
|
||||
@ -19,7 +19,11 @@ class TestEditCustomSlideForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = EditCustomSlideForm()
|
||||
@ -30,7 +34,6 @@ class TestEditCustomSlideForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def basic_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ Package to test the openlp.plugins.songs.forms.authorsform package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.songs.forms.authorsform import AuthorsForm
|
||||
@ -19,7 +19,11 @@ class TestAuthorsForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = AuthorsForm()
|
||||
@ -30,7 +34,6 @@ class TestAuthorsForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def ui_defaults_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ Package to test the openlp.plugins.songs.forms.editsongform package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
@ -20,7 +20,11 @@ class TestEditSongForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
Registry().register('theme_manager', MagicMock())
|
||||
@ -32,7 +36,6 @@ class TestEditSongForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def ui_defaults_test(self):
|
||||
"""
|
||||
|
@ -19,7 +19,11 @@ class TestEditVerseForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = EditVerseForm()
|
||||
@ -30,7 +34,6 @@ class TestEditVerseForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def ui_defaults_test(self):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ Package to test the openlp.plugins.songs.forms.topicsform package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.songs.forms.topicsform import TopicsForm
|
||||
@ -19,7 +19,11 @@ class TestTopicsForm(TestCase):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.app = QtGui.QApplication([])
|
||||
old_app_instance = QtCore.QCoreApplication.instance()
|
||||
if old_app_instance is None:
|
||||
self.app = QtGui.QApplication([])
|
||||
else:
|
||||
self.app = old_app_instance
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = TopicsForm()
|
||||
@ -30,7 +34,6 @@ class TestTopicsForm(TestCase):
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def ui_defaults_test(self):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user