diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 2bc4027b6..073d3c7f7 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -72,15 +72,15 @@ class AppLocation(object): :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir* """ if dir_type == AppLocation.AppDir: - return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0]) + return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__)) elif dir_type == AppLocation.PluginsDir: - app_path = os.path.abspath(os.path.split(sys.argv[0])[0]) + app_path = os.path.abspath(os.path.dirname(sys.argv[0])) return get_frozen_path(os.path.join(app_path, 'plugins'), - os.path.join(os.path.split(openlp.__file__)[0], 'plugins')) + os.path.join(os.path.dirname(openlp.__file__), 'plugins')) elif dir_type == AppLocation.VersionDir: - return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0]) + return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__)) elif dir_type == AppLocation.LanguageDir: - app_path = get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), _get_os_dir_path(dir_type)) + app_path = get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), _get_os_dir_path(dir_type)) return os.path.join(app_path, 'i18n') elif dir_type == AppLocation.DataDir and AppLocation.BaseDir: return os.path.join(AppLocation.BaseDir, 'data') @@ -140,18 +140,22 @@ def _get_os_dir_path(dir_type): """ Return a path based on which OS and environment we are running in. """ + # If running from source, return the language directory from the source directory + if dir_type == AppLocation.LanguageDir: + directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')) + if os.path.exists(directory): + return directory if sys.platform == 'win32': if dir_type == AppLocation.DataDir: return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data') elif dir_type == AppLocation.LanguageDir: - return os.path.split(openlp.__file__)[0] + return os.path.dirname(openlp.__file__) return os.path.join(str(os.getenv('APPDATA')), 'openlp') elif sys.platform == 'darwin': if dir_type == AppLocation.DataDir: - return os.path.join(str(os.getenv('HOME')), - 'Library', 'Application Support', 'openlp', 'Data') + return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data') elif dir_type == AppLocation.LanguageDir: - return os.path.split(openlp.__file__)[0] + return os.path.dirname(openlp.__file__) return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp') else: if dir_type == AppLocation.LanguageDir: diff --git a/openlp/core/lib/filedialog.py b/openlp/core/lib/filedialog.py index 5bf012ee5..3d1970dd5 100644 --- a/openlp/core/lib/filedialog.py +++ b/openlp/core/lib/filedialog.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 ############################################################################### # OpenLP - Open Source Lyrics Projection # diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 4d7676ad6..5893e8c38 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -495,8 +495,8 @@ s if service_item: service_item.from_plugin = True self.preview_controller.add_service_item(service_item) - if keep_focus: - self.list_view.setFocus() + if not keep_focus: + self.preview_controller.preview_widget.setFocus() def on_live_click(self): """ @@ -535,6 +535,7 @@ s if remote: service_item.will_auto_start = True self.live_controller.add_service_item(service_item) + self.live_controller.preview_widget.setFocus() def create_item_from_id(self, item_id): """ diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 474113c98..822d510b2 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -82,11 +82,6 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): present_plugin_dir = os.path.join(self.base_path, 'presentations') self.log_debug('finding plugins in %s at depth %d' % (self.base_path, start_depth)) for root, dirs, files in os.walk(self.base_path): - if sys.platform == 'darwin' and root.startswith(present_plugin_dir): - # TODO Presentation plugin is not yet working on Mac OS X. - # For now just ignore it. The following code will ignore files from the presentation plugin directory - # and thereby never import the plugin. - continue for name in files: if name.endswith('.py') and not name.startswith('__'): path = os.path.abspath(os.path.join(root, name)) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index 965adb053..a1e37abcf 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -295,7 +295,7 @@ def set_case_insensitive_completer(cache, widget): Sets a case insensitive text completer for a widget. :param cache: The list of items to use as suggestions. - :param widget: A widget to set the completer (QComboBox or QTextEdit instance) + :param widget: A widget to set the completer (QComboBox or QLineEdit instance) """ completer = QtGui.QCompleter(cache) completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 531c4b49f..d7c16f0d3 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -412,10 +412,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): self._increment_progress_bar(translate('OpenLP.FirstTimeWizard', 'Enabling selected plugins...')) self._set_plugin_status(self.songs_check_box, 'songs/status') self._set_plugin_status(self.bible_check_box, 'bibles/status') - # TODO Presentation plugin is not yet working on Mac OS X. - # For now just ignore it. - if sys.platform != 'darwin': - self._set_plugin_status(self.presentation_check_box, 'presentations/status') + self._set_plugin_status(self.presentation_check_box, 'presentations/status') self._set_plugin_status(self.image_check_box, 'images/status') self._set_plugin_status(self.media_check_box, 'media/status') self._set_plugin_status(self.remote_check_box, 'remotes/status') diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 35623ded2..ff1675ff5 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -95,13 +95,10 @@ class Ui_FirstTimeWizard(object): self.image_check_box.setChecked(True) self.image_check_box.setObjectName('image_check_box') self.plugin_layout.addWidget(self.image_check_box) - # TODO Presentation plugin is not yet working on Mac OS X. - # For now just ignore it. - if sys.platform != 'darwin': - self.presentation_check_box = QtGui.QCheckBox(self.plugin_page) - self.presentation_check_box.setChecked(True) - self.presentation_check_box.setObjectName('presentation_check_box') - self.plugin_layout.addWidget(self.presentation_check_box) + self.presentation_check_box = QtGui.QCheckBox(self.plugin_page) + self.presentation_check_box.setChecked(True) + self.presentation_check_box.setObjectName('presentation_check_box') + self.plugin_layout.addWidget(self.presentation_check_box) self.media_check_box = QtGui.QCheckBox(self.plugin_page) self.media_check_box.setChecked(True) self.media_check_box.setObjectName('media_check_box') @@ -222,10 +219,7 @@ class Ui_FirstTimeWizard(object): self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Custom Slides')) self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Bible')) self.image_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Images')) - # TODO Presentation plugin is not yet working on Mac OS X. - # For now just ignore it. - if sys.platform != 'darwin': - self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Presentations')) + self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Presentations')) self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media (Audio and Video)')) self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Allow remote access')) self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Monitor Song Usage')) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index be4247bc1..4f3d5d251 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -91,10 +91,9 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont """ new_row = self.tag_table_widget.rowCount() self.tag_table_widget.insertRow(new_row) - self.tag_table_widget.setItem(new_row, 0, - QtGui.QTableWidgetItem(translate('OpenLP.FormattingTagForm', 'New Tag%s') - % str(new_row))) - self.tag_table_widget.setItem(new_row, 1, QtGui.QTableWidgetItem('n%s' % str(new_row))) + self.tag_table_widget.setItem(new_row, 0, QtGui.QTableWidgetItem(translate('OpenLP.FormattingTagForm', + 'New Tag %d' % new_row))) + self.tag_table_widget.setItem(new_row, 1, QtGui.QTableWidgetItem('n%d' % new_row)) self.tag_table_widget.setItem(new_row, 2, QtGui.QTableWidgetItem(translate('OpenLP.FormattingTagForm', ''))) self.tag_table_widget.setItem(new_row, 3, QtGui.QTableWidgetItem('')) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 7dbcfc2bd..831eb182b 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -136,7 +136,6 @@ class ListPreviewWidget(QtGui.QTableWidget, RegistryProperties): if self.service_item.is_text(): self.resizeRowsToContents() self.setColumnWidth(0, self.viewport().width()) - self.setFocus() self.change_slide(slide_number) def change_slide(self, slide): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 664c1a596..6894293ce 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -598,7 +598,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): if self.arguments: self.open_cmd_line_files() elif Settings().value(self.general_settings_section + '/auto open'): - self.service_manager_contents.load_Last_file() + self.service_manager_contents.load_last_file() self.timer_version_id = self.startTimer(1000) view_mode = Settings().value('%s/view mode' % self.general_settings_section) if view_mode == 'default': diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 70cbd6141..52afb5edc 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -401,9 +401,12 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage :param suffix_list: New Suffix's to be supported """ - for suffix in suffix_list: - if suffix not in self.suffixes: - self.suffixes.append(suffix) + if isinstance(suffix_list, str): + self.suffixes.append(suffix_list) + else: + for suffix in suffix_list: + if suffix not in self.suffixes: + self.suffixes.append(suffix) def on_new_service_clicked(self, field=None): """ @@ -1081,6 +1084,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage :param field: :param message: The data passed in from a remove message """ + self.log_debug(message) self.set_item(int(message)) def set_item(self, index): @@ -1089,7 +1093,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage :param index: The index of the service item list to be actioned. """ - if 0 >= index < self.service_manager_list.topLevelItemCount(): + if 0 <= index < self.service_manager_list.topLevelItemCount(): item = self.service_manager_list.topLevelItem(index) self.service_manager_list.setCurrentItem(item) self.make_live() diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 321071c49..46fd227dd 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -18,11 +18,11 @@ # Software Foundation; version 2 of the License. # # # # This program is distributed in the hope that it will be useful, but WITHOUT # -# AN_y WARRANT_y; without even the implied warranty of MERCHANTABILIT_y or # +# 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 # +# 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 # ############################################################################### @@ -179,7 +179,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): if self.page(self.currentId()) == self.background_page and \ self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename): QtGui.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), - translate('OpenLP.ThemeWizard', '_you have not selected a ' + translate('OpenLP.ThemeWizard', 'You have not selected a ' 'background image. Please select one before continuing.')) return False else: diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 908981dc6..23bc0a9e1 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -51,6 +51,7 @@ class WizardStrings(object): CSV = 'CSV' OS = 'OpenSong' OSIS = 'OSIS' + ZEF = 'Zefania' # These strings should need a good reason to be retranslated elsewhere. FinishedImport = translate('OpenLP.Ui', 'Finished import.') FormatLabel = translate('OpenLP.Ui', 'Format:') diff --git a/openlp/core/utils/languagemanager.py b/openlp/core/utils/languagemanager.py index bb584f7bd..dd048e04c 100644 --- a/openlp/core/utils/languagemanager.py +++ b/openlp/core/utils/languagemanager.py @@ -71,8 +71,7 @@ class LanguageManager(object): """ Find all available language files in this OpenLP install """ - log.debug('Translation files: %s', AppLocation.get_directory( - AppLocation.LanguageDir)) + log.debug('Translation files: %s', AppLocation.get_directory(AppLocation.LanguageDir)) trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir)) file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name) # Remove qm files from the list which start with "qt_". diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 79b0bc699..171994d18 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -110,6 +110,7 @@ class BibleImportForm(OpenLPWizard): self.csv_books_button.clicked.connect(self.on_csv_books_browse_button_clicked) self.csv_verses_button.clicked.connect(self.on_csv_verses_browse_button_clicked) self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked) + self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked) def add_custom_pages(self): """ @@ -125,7 +126,7 @@ class BibleImportForm(OpenLPWizard): self.format_label = QtGui.QLabel(self.select_page) self.format_label.setObjectName('FormatLabel') self.format_combo_box = QtGui.QComboBox(self.select_page) - self.format_combo_box.addItems(['', '', '', '']) + self.format_combo_box.addItems(['', '', '', '', '']) self.format_combo_box.setObjectName('FormatComboBox') self.format_layout.addRow(self.format_label, self.format_combo_box) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) @@ -247,6 +248,25 @@ class BibleImportForm(OpenLPWizard): self.web_proxy_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.web_password_edit) self.web_tab_widget.addTab(self.web_proxy_tab, '') self.select_stack.addWidget(self.web_tab_widget) + self.zefania_widget = QtGui.QWidget(self.select_page) + self.zefania_widget.setObjectName('ZefaniaWidget') + self.zefania_layout = QtGui.QFormLayout(self.zefania_widget) + self.zefania_layout.setMargin(0) + self.zefania_layout.setObjectName('ZefaniaLayout') + self.zefania_file_label = QtGui.QLabel(self.zefania_widget) + self.zefania_file_label.setObjectName('ZefaniaFileLabel') + self.zefania_file_layout = QtGui.QHBoxLayout() + self.zefania_file_layout.setObjectName('ZefaniaFileLayout') + self.zefania_file_edit = QtGui.QLineEdit(self.zefania_widget) + self.zefania_file_edit.setObjectName('ZefaniaFileEdit') + self.zefania_file_layout.addWidget(self.zefania_file_edit) + self.zefania_browse_button = QtGui.QToolButton(self.zefania_widget) + self.zefania_browse_button.setIcon(self.open_icon) + self.zefania_browse_button.setObjectName('ZefaniaBrowseButton') + self.zefania_file_layout.addWidget(self.zefania_browse_button) + self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout) + self.zefania_layout.setItem(5, QtGui.QFormLayout.LabelRole, self.spacer) + self.select_stack.addWidget(self.zefania_widget) self.select_page_layout.addLayout(self.select_stack) self.addPage(self.select_page) # License Page @@ -294,11 +314,13 @@ class BibleImportForm(OpenLPWizard): self.format_combo_box.setItemText(BibleFormat.OpenSong, WizardStrings.OS) self.format_combo_box.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm', 'Web Download')) + self.format_combo_box.setItemText(BibleFormat.Zefania, WizardStrings.ZEF) self.osis_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.csv_books_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Books file:')) self.csv_verses_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Verses file:')) self.open_song_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_source_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Location:')) + self.zefania_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_source_combo_box.setItemText(WebDownload.Crosswalk, translate('BiblesPlugin.ImportWizardForm', 'Crosswalk')) self.web_source_combo_box.setItemText(WebDownload.BibleGateway, translate('BiblesPlugin.ImportWizardForm', @@ -331,7 +353,8 @@ class BibleImportForm(OpenLPWizard): self.osis_file_label.minimumSizeHint().width(), self.csv_books_label.minimumSizeHint().width(), self.csv_verses_label.minimumSizeHint().width(), - self.open_song_file_label.minimumSizeHint().width()) + self.open_song_file_label.minimumSizeHint().width(), + self.zefania_file_label.minimumSizeHint().width()) self.spacer.changeSize(label_width, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) def validateCurrentPage(self): @@ -366,6 +389,11 @@ class BibleImportForm(OpenLPWizard): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OS) self.open_song_file_edit.setFocus() return False + elif self.field('source_format') == BibleFormat.Zefania: + if not self.field('zefania_file'): + critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF) + self.zefania_file_edit.setFocus() + return False elif self.field('source_format') == BibleFormat.WebDownload: self.version_name_edit.setText(self.web_translation_combo_box.currentText()) return True @@ -447,6 +475,13 @@ class BibleImportForm(OpenLPWizard): self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OS, self.open_song_file_edit, 'last directory import') + def on_zefania_browse_button_clicked(self): + """ + Show the file open dialog for the Zefania file. + """ + self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.ZEF, self.zefania_file_edit, + 'last directory import') + def register_fields(self): """ Register the bible import wizard fields. @@ -456,6 +491,7 @@ class BibleImportForm(OpenLPWizard): self.select_page.registerField('csv_booksfile', self.csv_books_edit) self.select_page.registerField('csv_versefile', self.csv_verses_edit) self.select_page.registerField('opensong_file', self.open_song_file_edit) + self.select_page.registerField('zefania_file', self.zefania_file_edit) self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box) self.select_page.registerField('proxy_server', self.web_server_edit) @@ -479,6 +515,7 @@ class BibleImportForm(OpenLPWizard): self.setField('csv_booksfile', '') self.setField('csv_versefile', '') self.setField('opensong_file', '') + self.setField('zefania_file', '') self.setField('web_location', WebDownload.Crosswalk) self.setField('web_biblename', self.web_translation_combo_box.currentIndex()) self.setField('proxy_server', settings.value('proxy address')) @@ -562,6 +599,10 @@ class BibleImportForm(OpenLPWizard): proxy_username=self.field('proxy_username'), proxy_password=self.field('proxy_password') ) + elif bible_type == BibleFormat.Zefania: + # Import an Zefania bible. + importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, + filename=self.field('zefania_file')) if importer.do_import(license_version): self.manager.save_meta_data(license_version, license_version, license_copyright, license_permissions) self.manager.reload_bibles() diff --git a/openlp/plugins/bibles/lib/csvbible.py b/openlp/plugins/bibles/lib/csvbible.py index c7d37cb7b..9bffb25fe 100644 --- a/openlp/plugins/bibles/lib/csvbible.py +++ b/openlp/plugins/bibles/lib/csvbible.py @@ -149,7 +149,7 @@ class CSVBible(BibleDB): book_ptr = book.name self.wizard.increment_progress_bar( translate('BiblesPlugin.CSVBible', - 'Importing verses from %s... Importing verses from ...') % book.name) + 'Importing verses from %s...' % book.name, 'Importing verses from ...')) self.session.commit() try: verse_text = str(line[3], details['encoding']) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index c57fc117e..7dab8b3d7 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -38,6 +38,7 @@ from .csvbible import CSVBible from .http import HTTPBible from .opensong import OpenSongBible from .osis import OSISBible +from .zefania import ZefaniaBible log = logging.getLogger(__name__) @@ -52,6 +53,7 @@ class BibleFormat(object): CSV = 1 OpenSong = 2 WebDownload = 3 + Zefania = 4 @staticmethod def get_class(bible_format): @@ -68,6 +70,8 @@ class BibleFormat(object): return OpenSongBible elif bible_format == BibleFormat.WebDownload: return HTTPBible + elif bible_format == BibleFormat.Zefania: + return ZefaniaBible else: return None @@ -81,6 +85,7 @@ class BibleFormat(object): BibleFormat.CSV, BibleFormat.OpenSong, BibleFormat.WebDownload, + BibleFormar.Zefania, ] diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index c7bfa01a2..fa8323d7f 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -81,6 +81,13 @@ class OpenSongBible(BibleDB): import_file = open(self.filename, 'rb') opensong = objectify.parse(import_file) bible = opensong.getroot() + # Check that we're not trying to import a Zefania XML bible, it is sometimes refered to as 'OpenSong' + if bible.tag.upper() == 'XMLBIBLE': + critical_error_message_box( + message=translate('BiblesPlugin.OpenSongImport', + 'Incorrect Bible file type supplied. This looks like a Zefania XML bible, ' + 'please use the Zefania import option.')) + return False language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) diff --git a/openlp/plugins/bibles/lib/zefania.py b/openlp/plugins/bibles/lib/zefania.py new file mode 100644 index 000000000..4e8373a93 --- /dev/null +++ b/openlp/plugins/bibles/lib/zefania.py @@ -0,0 +1,113 @@ +# -*- 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 # +############################################################################### + +import logging +from lxml import etree, objectify + +from openlp.core.common import translate +from openlp.core.lib.ui import critical_error_message_box +from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB + + +log = logging.getLogger(__name__) + + +class ZefaniaBible(BibleDB): + """ + Zefania Bible format importer class. + """ + def __init__(self, parent, **kwargs): + """ + Constructor to create and set up an instance of the ZefaniaBible class. This class is used to import Bibles + from ZefaniaBible's XML format. + """ + log.debug(self.__class__.__name__) + BibleDB.__init__(self, parent, **kwargs) + self.filename = kwargs['filename'] + + def do_import(self, bible_name=None): + """ + Loads a Bible from file. + """ + log.debug('Starting Zefania import from "%s"' % self.filename) + if not isinstance(self.filename, str): + self.filename = str(self.filename, 'utf8') + import_file = None + success = True + try: + # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding + # detection, and the two mechanisms together interfere with each other. + import_file = open(self.filename, 'rb') + language_id = self.get_language(bible_name) + if not language_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + zefania_bible_tree = etree.parse(import_file) + num_books = int(zefania_bible_tree.xpath("count(//BIBLEBOOK)")) + # Strip tags we don't use - keep content + etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF')) + # Strip tags we don't use - remove content + etree.strip_elements(zefania_bible_tree, ('PROLOG', 'REMARK', 'CAPTION', 'MEDIA'), with_tail=False) + xmlbible = zefania_bible_tree.getroot() + for BIBLEBOOK in xmlbible: + book_ref_id = self.get_book_ref_id_by_name(str(BIBLEBOOK.get('bname')), num_books) + if not book_ref_id: + book_ref_id = self.get_book_ref_id_by_localised_name(str(BIBLEBOOK.get('bname'))) + if not book_ref_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) + db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) + for CHAPTER in BIBLEBOOK: + if self.stop_import_flag: + break + chapter_number = CHAPTER.get("cnumber") + for VERS in CHAPTER: + verse_number = VERS.get("vnumber") + self.create_verse(db_book.id, chapter_number, verse_number, VERS.text.replace('
', '\n')) + self.wizard.increment_progress_bar( + translate('BiblesPlugin.Zefnia', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) + self.session.commit() + self.application.process_events() + except Exception as e: + print(str(e)) + critical_error_message_box( + message=translate('BiblesPlugin.ZefaniaImport', + 'Incorrect Bible file type supplied. Zefania Bibles may be ' + 'compressed. You must decompress them before import.')) + log.exception(str(e)) + success = False + finally: + if import_file: + import_file.close() + if self.stop_import_flag: + return False + else: + return success diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 5ed11d2ab..b98ae131a 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -126,8 +126,8 @@ class PdfController(PresentationController): if os.name == 'nt': # for windows we only accept mudraw.exe in the base folder application_path = AppLocation.get_directory(AppLocation.AppDir) - if os.path.isfile(application_path + '/../mudraw.exe'): - self.mudrawbin = application_path + '/../mudraw.exe' + if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): + self.mudrawbin = os.path.join(application_path, 'mudraw.exe') else: DEVNULL = open(os.devnull, 'wb') # First try to find mupdf @@ -145,8 +145,8 @@ class PdfController(PresentationController): # Last option: check if mudraw is placed in OpenLP base folder if not self.mudrawbin and not self.gsbin: application_path = AppLocation.get_directory(AppLocation.AppDir) - if os.path.isfile(application_path + '/../mudraw'): - self.mudrawbin = application_path + '/../mudraw' + if os.path.isfile(os.path.join(application_path, 'mudraw')): + self.mudrawbin = os.path.join(application_path, 'mudraw') if self.mudrawbin: self.also_supports = ['xps'] return True diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index ae47a110f..2125922fe 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -42,7 +42,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStri from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box from openlp.plugins.songs.lib import VerseType, clean_song -from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorSong, AuthorType, Topic, MediaFile +from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorType, Topic, MediaFile from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog @@ -966,10 +966,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties): self.song.authors_songs = [] for row in range(self.authors_list_view.count()): item = self.authors_list_view.item(row) - author_song = AuthorSong() - author_song.author_id = item.data(QtCore.Qt.UserRole)[0] - author_song.author_type = item.data(QtCore.Qt.UserRole)[1] - self.song.authors_songs.append(author_song) + self.song.add_author(self.manager.get_object(Author, item.data(QtCore.Qt.UserRole)[0]), + item.data(QtCore.Qt.UserRole)[1]) self.song.topics = [] for row in range(self.topics_list_view.count()): item = self.topics_list_view.item(row) diff --git a/openlp/plugins/songs/forms/songmaintenanceform.py b/openlp/plugins/songs/forms/songmaintenanceform.py index 4e9bdad93..bdbae41fb 100644 --- a/openlp/plugins/songs/forms/songmaintenanceform.py +++ b/openlp/plugins/songs/forms/songmaintenanceform.py @@ -400,7 +400,7 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog, RegistryPrope """ Merges two authors into one author. - :param old_author: The object, which was edited, that will be deleted + :param old_author: The object, which was edited, that will be deleted """ # Find the duplicate. existing_author = self.manager.get_object_filtered( @@ -415,11 +415,9 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog, RegistryPrope # Find the songs, which have the old_author as author. songs = self.manager.get_all_objects(Song, Song.authors.contains(old_author)) for song in songs: - # We check if the song has already existing_author as author. If - # that is not the case we add it. - if existing_author not in song.authors: - song.authors.append(existing_author) - song.authors.remove(old_author) + for author_song in song.authors_songs: + song.add_author(existing_author, author_song.author_type) + song.remove_author(old_author, author_song.author_type) self.manager.save_object(song) self.manager.delete_object(Author, old_author.id) diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index ec14175ac..d03bdefd6 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -278,7 +278,7 @@ class VerseType(object): if verse_index is None: verse_index = VerseType.from_string(verse_name, default) elif len(verse_name) == 1: - verse_index = VerseType.from_translated_tag(verse_name, None) + verse_index = VerseType.from_translated_tag(verse_name, default) if verse_index is None: verse_index = VerseType.from_tag(verse_name, default) else: @@ -390,12 +390,12 @@ def clean_song(manager, song): verses = SongXML().get_verses(song.lyrics) song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses]) # The song does not have any author, add one. - if not song.authors and not song.authors_songs: # Need to check both relations + if not song.authors_songs: name = SongStrings.AuthorUnknown author = manager.get_object_filtered(Author, Author.display_name == name) if author is None: author = Author.populate(display_name=name, last_name='', first_name='') - song.authors.append(author) + song.add_author(author) if song.copyright: song.copyright = CONTROL_CHARS.sub('', song.copyright).strip() diff --git a/openlp/plugins/songs/lib/cclifileimport.py b/openlp/plugins/songs/lib/cclifileimport.py index 5c1fbdcb2..eda235ca1 100644 --- a/openlp/plugins/songs/lib/cclifileimport.py +++ b/openlp/plugins/songs/lib/cclifileimport.py @@ -63,7 +63,6 @@ class CCLIFileImport(SongImport): for filename in self.import_source: filename = str(filename) log.debug('Importing CCLI File: %s', filename) - lines = [] if os.path.isfile(filename): detect_file = open(filename, 'r') detect_content = detect_file.read(2048) @@ -250,7 +249,7 @@ class CCLIFileImport(SongImport): # e.g. For use solely with the SongSelect Terms of Use. All rights Reserved. www.ccli.com CCLI Licence number of user - # e.g. CCL-Liedlizenznummer: 14 / CCLI License No. 14 + # e.g. CCLI-Liedlizenznummer: 14 / CCLI License No. 14 """ log.debug('TXT file text: %s', text_list) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 91649c951..16f7ea719 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -74,10 +74,11 @@ class AuthorType(object): WordsAndMusic = 'words+music' Translation = 'translation' Types = { - Words: translate('OpenLP.Ui', 'Words'), - Music: translate('OpenLP.Ui', 'Music'), - WordsAndMusic: translate('OpenLP.Ui', 'Words and Music'), - Translation: translate('OpenLP.Ui', 'Translation') + Words: translate('SongsPlugin.AuthorType', 'Words', 'Author who wrote the lyrics of a song'), + Music: translate('SongsPlugin.AuthorType', 'Music', 'Author who wrote the music of a song'), + WordsAndMusic: translate('SongsPlugin.AuthorType', 'Words and Music', + 'Author who wrote both lyrics and music of a song'), + Translation: translate('SongsPlugin.AuthorType', 'Translation', 'Author who translated the song') } @@ -114,6 +115,33 @@ class Song(BaseModel): """ self.sort_key = get_natural_key(self.title) + def add_author(self, author, author_type=None): + """ + Add an author to the song if it not yet exists + + :param author: Author object + :param author_type: AuthorType constant or None + """ + for author_song in self.authors_songs: + if author_song.author == author and author_song.author_type == author_type: + return + new_author_song = AuthorSong() + new_author_song.author = author + new_author_song.author_type = author_type + self.authors_songs.append(new_author_song) + + def remove_author(self, author, author_type=None): + """ + Remove an existing author from the song + + :param author: Author object + :param author_type: AuthorType constant or None + """ + for author_song in self.authors_songs: + if author_song.author == author and author_song.author_type == author_type: + self.authors_songs.remove(author_song) + return + class Topic(BaseModel): """ @@ -283,9 +311,10 @@ def init_schema(url): mapper(Book, song_books_table) mapper(MediaFile, media_files_table) mapper(Song, songs_table, properties={ - # Use the authors_songs relation when you need access to the 'author_type' attribute. + # Use the authors_songs relation when you need access to the 'author_type' attribute + # or when creating new relations 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"), - 'authors': relation(Author, secondary=authors_songs_table), + 'authors': relation(Author, secondary=authors_songs_table, viewonly=True), 'book': relation(Book, backref='songs'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index b08193672..c56e1dba1 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -200,11 +200,20 @@ class EasyWorshipSongImport(SongImport): Import the songs from the database """ # Open the DB and MB files if they exist - import_source_mb = self.import_source.replace('.DB', '.MB') - if not os.path.isfile(self.import_source) or not os.path.isfile(import_source_mb): + import_source_mb = self.import_source.replace('.DB', '.MB').replace('.db', '.mb') + if not os.path.isfile(self.import_source): + self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', + 'This file does not exist.')) + return + if not os.path.isfile(import_source_mb): + self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', + 'Could not find the "Songs.MB" file. It must be in the same ' + 'folder as the "Songs.DB" file.')) return db_size = os.path.getsize(self.import_source) if db_size < 0x800: + self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', + 'This file is not a valid EasyWorship database.')) return db_file = open(self.import_source, 'rb') self.memo_file = open(import_source_mb, 'rb') @@ -213,6 +222,8 @@ class EasyWorshipSongImport(SongImport): if header_size != 0x800 or block_size < 1 or block_size > 4: db_file.close() self.memo_file.close() + self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', + 'This file is not a valid EasyWorship database.')) return # Take a stab at how text is encoded self.encoding = 'cp1252' @@ -240,6 +251,8 @@ class EasyWorshipSongImport(SongImport): self.encoding = 'cp874' self.encoding = retrieve_windows_encoding(self.encoding) if not self.encoding: + self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', + 'Could not retrieve encoding.')) return # Read the field description information db_file.seek(120) @@ -344,7 +357,7 @@ class EasyWorshipSongImport(SongImport): try: decoded_words = words.decode() except UnicodeDecodeError: - # The unicode chars in the rtf was not escaped in the expected manor + # The unicode chars in the rtf was not escaped in the expected manner self.entry_error_log = translate('SongsPlugin.EasyWorshipSongImport', 'Unexpected data formatting.') return diff --git a/openlp/plugins/songs/lib/foilpresenterimport.py b/openlp/plugins/songs/lib/foilpresenterimport.py index 6f8bd6978..2b31718c2 100644 --- a/openlp/plugins/songs/lib/foilpresenterimport.py +++ b/openlp/plugins/songs/lib/foilpresenterimport.py @@ -343,7 +343,7 @@ class FoilPresenter(object): author = Author.populate(display_name=display_name, last_name=display_name.split(' ')[-1], first_name=' '.join(display_name.split(' ')[:-1])) self.manager.save_object(author) - song.authors.append(author) + song.add_author(author) def _process_cclinumber(self, foilpresenterfolie, song): """ diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index ed32a43ee..7ce637285 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -49,6 +49,7 @@ from .songproimport import SongProImport from .sundayplusimport import SundayPlusImport from .foilpresenterimport import FoilPresenterImport from .zionworximport import ZionWorxImport +from .propresenterimport import ProPresenterImport # Imports that might fail @@ -159,14 +160,15 @@ class SongFormat(object): MediaShout = 9 OpenSong = 10 PowerSong = 11 - SongBeamer = 12 - SongPro = 13 - SongShowPlus = 14 - SongsOfFellowship = 15 - SundayPlus = 16 - WordsOfWorship = 17 - WorshipCenterPro = 18 - ZionWorx = 19 + ProPresenter = 12 + SongBeamer = 13 + SongPro = 14 + SongShowPlus = 15 + SongsOfFellowship = 16 + SundayPlus = 17 + WordsOfWorship = 18 + WorshipCenterPro = 19 + ZionWorx = 20 # Set optional attribute defaults __defaults__ = { @@ -270,6 +272,12 @@ class SongFormat(object): 'invalidSourceMsg': translate('SongsPlugin.ImportWizardForm', 'You need to specify a valid PowerSong 1.0 ' 'database folder.') }, + ProPresenter: { + 'class': ProPresenterImport, + 'name': 'ProPresenter', + 'prefix': 'proPresenter', + 'filter': '%s (*.pro4)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter Song Files') + }, SongBeamer: { 'class': SongBeamerImport, 'name': 'SongBeamer', @@ -355,6 +363,7 @@ class SongFormat(object): SongFormat.MediaShout, SongFormat.OpenSong, SongFormat.PowerSong, + SongFormat.ProPresenter, SongFormat.SongBeamer, SongFormat.SongPro, SongFormat.SongShowPlus, diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index bee0e0f0d..d57c8fbcc 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -527,15 +527,7 @@ class SongMediaItem(MediaManagerItem): add_song = True if search_results: for song in search_results: - author_list = item.data_string['authors'] - same_authors = True - for author in song.authors: - if author.display_name in author_list: - author_list = author_list.replace(author.display_name, '', 1) - else: - same_authors = False - break - if same_authors and author_list.strip(', ') == '': + if self._authors_match(song, item.data_string['authors']): add_song = False edit_id = song.id break @@ -561,6 +553,23 @@ class SongMediaItem(MediaManagerItem): self.generate_footer(item, song) return item + def _authors_match(self, song, authors): + """ + Checks whether authors from a song in the database match the authors of the song to be imported. + + :param song: A list of authors from the song in the database + :param authors: A string with authors from the song to be imported + :return: True when Authors do match, else False. + """ + author_list = authors.split(', ') + for author in song.authors: + if author.display_name in author_list: + author_list.remove(author.display_name) + else: + return False + # List must be empty at the end + return not author_list + def search(self, string, show_error): """ Search for some songs diff --git a/openlp/plugins/songs/lib/olpimport.py b/openlp/plugins/songs/lib/olpimport.py index 335ba606a..f4b066ef0 100644 --- a/openlp/plugins/songs/lib/olpimport.py +++ b/openlp/plugins/songs/lib/olpimport.py @@ -187,7 +187,7 @@ class OpenLPSongImport(SongImport): first_name=author.first_name, last_name=author.last_name, display_name=author.display_name) - new_song.authors.append(existing_author) + new_song.add_author(existing_author) if song.book: existing_song_book = self.manager.get_object_filtered(Book, Book.name == song.book.name) if existing_song_book is None: diff --git a/openlp/plugins/songs/lib/opensongimport.py b/openlp/plugins/songs/lib/opensongimport.py index bee0989a0..3d9733dd8 100644 --- a/openlp/plugins/songs/lib/opensongimport.py +++ b/openlp/plugins/songs/lib/opensongimport.py @@ -45,11 +45,11 @@ class OpenSongImport(SongImport): """ Import songs exported from OpenSong - The format is described loosly on the `OpenSong File Format Specification + The format is described loosely on the `OpenSong File Format Specification `_ page on the OpenSong web site. However, it doesn't describe the section, so here's an attempt: - If the first charachter of a line is a space, then the rest of that line is lyrics. If it is not a space the + If the first character of a line is a space, then the rest of that line is lyrics. If it is not a space the following applies. Verses can be expressed in one of 2 ways, either in complete verses, or by line grouping, i.e. grouping all line 1's @@ -93,12 +93,19 @@ class OpenSongImport(SongImport): All verses are imported and tagged appropriately. - Guitar chords can be provided "above" the lyrics (the line is preceeded by a period "."), and one or more "_" can + Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example:: . A7 Bm 1 Some____ Words + Lines that contain only whitespace are ignored. + | indicates a blank line, and || a new slide. + + Slide 1 Line 1|Slide 1 Line 2||Slide 2 Line 1|Slide 2 Line 2 + + Lines beginning with ; are comments + The tag is used to populate the OpenLP verse display order field. The Author and Copyright tags are also imported to the appropriate places. """ @@ -107,9 +114,14 @@ class OpenSongImport(SongImport): """ Initialise the class. """ - SongImport.__init__(self, manager, **kwargs) + super(OpenSongImport, self).__init__(manager, **kwargs) def do_import(self): + """ + Receive a single file or a list of files to import. + """ + if not isinstance(self.import_source, list): + return self.import_wizard.progress_bar.setMaximum(len(self.import_source)) for filename in self.import_source: if self.stop_import_flag: @@ -141,19 +153,41 @@ class OpenSongImport(SongImport): 'author': self.parse_author, 'title': 'title', 'aka': 'alternate_title', - 'hymn_number': 'song_number' + 'hymn_number': self.parse_song_book_name_and_number, + 'user1': self.add_comment, + 'user2': self.add_comment, + 'user3': self.add_comment } for attr, fn_or_string in list(decode.items()): if attr in fields: ustring = str(root.__getattr__(attr)) if isinstance(fn_or_string, str): - setattr(self, fn_or_string, ustring) + if attr in ['ccli']: + if ustring: + setattr(self, fn_or_string, int(ustring)) + else: + setattr(self, fn_or_string, None) + else: + setattr(self, fn_or_string, ustring) else: fn_or_string(ustring) - if 'theme' in fields and str(root.theme) not in self.topics: - self.topics.append(str(root.theme)) - if 'alttheme' in fields and str(root.alttheme) not in self.topics: - self.topics.append(str(root.alttheme)) + # Themes look like "God: Awe/Wonder", but we just want + # "Awe" and "Wonder". We use a set to ensure each topic + # is only added once, in case it is already there, which + # is actually quite likely if the alttheme is set + topics = set(self.topics) + if 'theme' in fields: + theme = str(root.theme) + subthemes = theme[theme.find(':')+1:].split('/') + for topic in subthemes: + topics.add(topic.strip()) + if 'alttheme' in fields: + theme = str(root.alttheme) + subthemes = theme[theme.find(':')+1:].split('/') + for topic in subthemes: + topics.add(topic.strip()) + self.topics = list(topics) + self.topics.sort() # data storage while importing verses = {} # keep track of verses appearance order @@ -168,7 +202,7 @@ class OpenSongImport(SongImport): else: lyrics = '' for this_line in lyrics.split('\n'): - if not this_line: + if not this_line.strip(): continue # skip this line if it is a comment if this_line.startswith(';'): @@ -209,8 +243,14 @@ class OpenSongImport(SongImport): # Tidy text and remove the ____s from extended words this_line = self.tidy_text(this_line) this_line = this_line.replace('_', '') - this_line = this_line.replace('|', '\n') + this_line = this_line.replace('||', '\n[---]\n') this_line = this_line.strip() + # If the line consists solely of a '|', then just use the implicit newline + # Otherwise, add a newline for each '|' + if this_line == '|': + this_line = '' + else: + this_line = this_line.replace('|', '\n') verses[verse_tag][verse_num][inst].append(this_line) # done parsing # add verses in original order @@ -223,7 +263,14 @@ class OpenSongImport(SongImport): verse_def = '%s%s' % (verse_tag, verse_num[:length]) verse_joints[verse_def] = '%s\n[---]\n%s' % (verse_joints[verse_def], lines) \ if verse_def in verse_joints else lines - for verse_def, lines in verse_joints.items(): + # Parsing the dictionary produces the elements in a non-intuitive order. While it "works", it's not a + # natural layout should the user come back to edit the song. Instead we sort by the verse type, so that we + # get all the verses in order (v1, v2, ...), then the chorus(es), bridge(s), pre-chorus(es) etc. We use a + # tuple for the key, since tuples naturally sort in this manner. + verse_defs = sorted(verse_joints.keys(), + key=lambda verse_def: (VerseType.from_tag(verse_def[0]), int(verse_def[1:]))) + for verse_def in verse_defs: + lines = verse_joints[verse_def] self.add_verse(lines, verse_def) if not self.verses: self.add_verse('') @@ -244,6 +291,8 @@ class OpenSongImport(SongImport): # Assume it's no.1 if there are no digits verse_tag = verse_def verse_num = '1' + verse_index = VerseType.from_loose_input(verse_tag) + verse_tag = VerseType.tags[verse_index] verse_def = '%s%s' % (verse_tag, verse_num) if verse_num in verses.get(verse_tag, {}): self.verse_order_list.append(verse_def) diff --git a/openlp/plugins/songs/lib/propresenterimport.py b/openlp/plugins/songs/lib/propresenterimport.py new file mode 100644 index 000000000..6ce3c0819 --- /dev/null +++ b/openlp/plugins/songs/lib/propresenterimport.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 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:`propresenterimport` module provides the functionality for importing +ProPresenter song files into the current installation database. +""" + +import os +import base64 +from lxml import objectify + +from openlp.core.ui.wizard import WizardStrings +from openlp.plugins.songs.lib import strip_rtf +from .songimport import SongImport + + +class ProPresenterImport(SongImport): + """ + The :class:`ProPresenterImport` class provides OpenLP with the + ability to import ProPresenter song files. + """ + def do_import(self): + self.import_wizard.progress_bar.setMaximum(len(self.import_source)) + for file_path in self.import_source: + if self.stop_import_flag: + return + self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path)) + root = objectify.parse(open(file_path, 'rb')).getroot() + self.process_song(root) + + def process_song(self, root): + self.set_defaults() + self.title = root.get('CCLISongTitle') + self.copyright = root.get('CCLICopyrightInfo') + self.comments = root.get('notes') + self.ccli_number = root.get('CCLILicenseNumber') + for author_key in ['author', 'artist', 'CCLIArtistCredits']: + author = root.get(author_key) + if len(author) > 0: + self.parse_author(author) + count = 0 + for slide in root.slides.RVDisplaySlide: + count += 1 + RTFData = slide.displayElements.RVTextElement.get('RTFData') + rtf = base64.standard_b64decode(RTFData) + words, encoding = strip_rtf(rtf.decode()) + self.add_verse(words, "v%d" % count) + if not self.finish(): + self.log_error(self.import_source) diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index a5fbb99e0..b8fcc604b 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -188,13 +188,61 @@ class SongImport(QtCore.QObject): self.title = lines[0] self.add_verse(text) + def parse_song_book_name_and_number(self, book_and_number): + """ + Build the book name and song number from a single string + """ + # Turn 'Spring Harvest 1997 No. 34' or + # 'Spring Harvest 1997 (34)' or + # 'Spring Harvest 1997 34' into + # Book name:'Spring Harvest 1997' and + # Song number: 34 + # + # Also, turn 'NRH231.' into + # Book name:'NRH' and + # Song number: 231 + book_and_number = book_and_number.strip() + if not book_and_number: + return + book_and_number = book_and_number.replace('No.', ' ') + if ' ' in book_and_number: + parts = book_and_number.split(' ') + self.song_book_name = ' '.join(parts[:-1]) + self.song_number = parts[-1].strip('()') + else: + # Something like 'ABC123.' + match = re.match(r'(.*\D)(\d+)', book_and_number) + match_num = re.match(r'(\d+)', book_and_number) + if match: + # Name and number + self.song_book_name = match.group(1) + self.song_number = match.group(2) + # These last two cases aren't tested yet, but + # are here in an attempt to do something vaguely + # sensible if we get a string in a different format + elif match_num: + # Number only + self.song_number = match_num.group(1) + else: + # Name only + self.song_book_name = book_and_number + + def add_comment(self, comment): + """ + Build the comments field + """ + if self.comments.find(comment) >= 0: + return + if comment: + self.comments += comment.strip() + '\n' + def add_copyright(self, copyright): """ Build the copyright field """ if self.copyright.find(copyright) >= 0: return - if self.copyright != '': + if self.copyright: self.copyright += ' ' self.copyright += copyright @@ -325,7 +373,7 @@ class SongImport(QtCore.QObject): author = Author.populate(display_name=author_text, last_name=author_text.split(' ')[-1], first_name=' '.join(author_text.split(' ')[:-1])) - song.authors.append(author) + song.add_author(author) if self.song_book_name: song_book = self.manager.get_object_filtered(Book, Book.name == self.song_book_name) if song_book is None: diff --git a/openlp/plugins/songs/lib/songselect.py b/openlp/plugins/songs/lib/songselect.py index 232cdb9cf..6fd084a47 100644 --- a/openlp/plugins/songs/lib/songselect.py +++ b/openlp/plugins/songs/lib/songselect.py @@ -196,13 +196,13 @@ class SongSelectImport(object): db_song.lyrics = song_xml.extract_xml() clean_song(self.db_manager, db_song) self.db_manager.save_object(db_song) - db_song.authors = [] + db_song.authors_songs = [] 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) + db_song.add_author(author) self.db_manager.save_object(db_song) return db_song diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index 87e5da21e..9458d6180 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -71,7 +71,7 @@ from lxml import etree, objectify from openlp.core.common import translate from openlp.core.lib import FormattingTags from openlp.plugins.songs.lib import VerseType, clean_song -from openlp.plugins.songs.lib.db import Author, AuthorSong, AuthorType, Book, Song, Topic +from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic from openlp.core.utils import get_application_version log = logging.getLogger(__name__) @@ -519,10 +519,7 @@ class OpenLyrics(object): author = Author.populate(display_name=display_name, last_name=display_name.split(' ')[-1], first_name=' '.join(display_name.split(' ')[:-1])) - author_song = AuthorSong() - author_song.author = author - author_song.author_type = author_type - song.authors_songs.append(author_song) + song.add_author(author, author_type) def _process_cclinumber(self, properties, song): """ @@ -664,7 +661,7 @@ class OpenLyrics(object): # OpenLyrics 0.8 uses
for new lines. Append text from "lines" element to verse text. if version > '0.7': text = self._process_lines_mixed_content(element) - # OpenLyrics version <= 0.7 contais elements to represent lines. First child element is tested. + # OpenLyrics version <= 0.7 contains elements to represent lines. First child element is tested. else: # Loop over the "line" elements removing comments and chords. for line in element: diff --git a/scripts/jenkins_script.py b/scripts/jenkins_script.py old mode 100644 new mode 100755 diff --git a/scripts/translation_utils.py b/scripts/translation_utils.py index ad3edcaa3..0074109dd 100755 --- a/scripts/translation_utils.py +++ b/scripts/translation_utils.py @@ -63,7 +63,7 @@ import webbrowser from optparse import OptionParser from PyQt4 import QtCore -SERVER_URL = 'http://www.transifex.net/api/2/project/openlp/' +SERVER_URL = 'http://www.transifex.net/api/2/project/openlp/resource/openlp-22x/' IGNORED_PATHS = ['scripts'] IGNORED_FILES = ['setup.py'] @@ -175,7 +175,7 @@ def run(command): process = QtCore.QProcess() process.start(command) while process.waitForReadyRead(): - print_verbose('ReadyRead: %s' % QtCore.QString(process.readAll())) + print_verbose('ReadyRead: %s' % process.readAll()) print_verbose('Error(s):\n%s' % process.readAllStandardError()) print_verbose('Output:\n%s' % process.readAllStandardOutput()) @@ -193,27 +193,26 @@ def download_translations(): if not password: password = getpass(' Transifex password: ') # First get the list of languages - url = SERVER_URL + 'resource/ents/' - base64string = base64.encodbytes('%s:%s' % (username, password))[:-1] - auth_header = 'Basic %s' % base64string - request = urllib.request.Request(url + '?details') + base64string = base64.encodebytes(('%s:%s' % (username, password)).encode())[:-1] + auth_header = 'Basic %s' % base64string.decode() + request = urllib.request.Request(SERVER_URL + '?details') request.add_header('Authorization', auth_header) - print_verbose('Downloading list of languages from: %s' % url) + print_verbose('Downloading list of languages from: %s' % SERVER_URL) try: json_response = urllib.request.urlopen(request) except urllib.error.HTTPError: print_quiet('Username or password incorrect.') return False - json_dict = json.loads(json_response.read()) + json_dict = json.loads(json_response.read().decode()) languages = [lang['code'] for lang in json_dict['available_languages']] for language in languages: - lang_url = url + 'translation/%s/?file' % language + lang_url = SERVER_URL + 'translation/%s/?file' % language request = urllib.request.Request(lang_url) request.add_header('Authorization', auth_header) filename = os.path.join(os.path.abspath('..'), 'resources', 'i18n', language + '.ts') print_verbose('Get Translation File: %s' % filename) response = urllib.request.urlopen(request) - fd = open(filename, 'w') + fd = open(filename, 'wb') fd.write(response.read()) fd.close() print_quiet(' Done.') @@ -261,7 +260,7 @@ def prepare_project(): lines.append('TRANSLATIONS += %s' % line) lines.sort() file = open(os.path.join(start_dir, 'openlp.pro'), 'w') - file.write('\n'.join(lines).encode('utf8')) + file.write('\n'.join(lines)) file.close() print_quiet(' Done.') diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..7b4bf5a3c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[pep8] +exclude=resources.py,vlc.py +max-line-length = 120 diff --git a/tests/functional/openlp_core_common/test_common.py b/tests/functional/openlp_core_common/test_common.py index ab2d11b3a..f52256c5c 100644 --- a/tests/functional/openlp_core_common/test_common.py +++ b/tests/functional/openlp_core_common/test_common.py @@ -32,7 +32,7 @@ Functional tests to test the AppLocation class and related methods. from unittest import TestCase -from openlp.core.common import de_hump, trace_error_handler +from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate from tests.functional import MagicMock, patch @@ -40,6 +40,45 @@ class TestCommonFunctions(TestCase): """ A test suite to test out various functions in the openlp.core.common module. """ + def check_directory_exists_test(self): + """ + Test the check_directory_exists() function + """ + with patch('openlp.core.lib.os.path.exists') as mocked_exists, \ + patch('openlp.core.lib.os.makedirs') as mocked_makedirs: + # GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists + directory_to_check = 'existing/directory' + + # WHEN: os.path.exists returns True and we check to see if the directory exists + mocked_exists.return_value = True + check_directory_exists(directory_to_check) + + # THEN: Only os.path.exists should have been called + mocked_exists.assert_called_with(directory_to_check) + self.assertIsNot(mocked_makedirs.called, 'os.makedirs should not have been called') + + # WHEN: os.path.exists returns False and we check the directory exists + mocked_exists.return_value = False + check_directory_exists(directory_to_check) + + # THEN: Both the mocked functions should have been called + mocked_exists.assert_called_with(directory_to_check) + mocked_makedirs.assert_called_with(directory_to_check) + + # WHEN: os.path.exists raises an IOError + mocked_exists.side_effect = IOError() + check_directory_exists(directory_to_check) + + # THEN: We shouldn't get an exception though the mocked exists has been called + mocked_exists.assert_called_with(directory_to_check) + + # WHEN: Some other exception is raised + mocked_exists.side_effect = ValueError() + + # THEN: check_directory_exists raises an exception + mocked_exists.assert_called_with(directory_to_check) + self.assertRaises(ValueError, check_directory_exists, directory_to_check) + def de_hump_conversion_test(self): """ Test the de_hump function with a class name @@ -81,3 +120,22 @@ class TestCommonFunctions(TestCase): # THEN: The mocked_logger.error() method should have been called with the correct parameters mocked_logger.error.assert_called_with( 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') + + def translate_test(self): + """ + Test the translate() function + """ + # GIVEN: A string to translate and a mocked Qt translate function + context = 'OpenLP.Tests' + text = 'Untranslated string' + comment = 'A comment' + encoding = 1 + n = 1 + mocked_translate = MagicMock(return_value='Translated string') + + # WHEN: we call the translate function + result = translate(context, text, comment, encoding, n, mocked_translate) + + # THEN: the translated string should be returned, and the mocked function should have been called + mocked_translate.assert_called_with(context, text, comment, encoding, n) + self.assertEqual('Translated string', result, 'The translated string should have been returned') diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 2e06e47fe..fca6f371d 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -36,9 +36,8 @@ from datetime import datetime, timedelta from PyQt4 import QtCore, QtGui -from openlp.core.common import check_directory_exists, translate -from openlp.core.lib import str_to_bool, create_thumb, get_text_file_string, \ - build_icon, image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags +from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \ + expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb from tests.functional import MagicMock, patch TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) @@ -152,64 +151,6 @@ class TestLib(TestCase): # THEN: we should get back a true self.assertTrue(str_result, 'The result should be True') - def translate_test(self): - """ - Test the translate() function - """ - # GIVEN: A string to translate and a mocked Qt translate function - context = 'OpenLP.Tests' - text = 'Untranslated string' - comment = 'A comment' - encoding = 1 - n = 1 - mocked_translate = MagicMock(return_value='Translated string') - - # WHEN: we call the translate function - result = translate(context, text, comment, encoding, n, mocked_translate) - - # THEN: the translated string should be returned, and the mocked function should have been called - mocked_translate.assert_called_with(context, text, comment, encoding, n) - self.assertEqual('Translated string', result, 'The translated string should have been returned') - - def check_directory_exists_test(self): - """ - Test the check_directory_exists() function - """ - with patch('openlp.core.lib.os.path.exists') as mocked_exists, \ - patch('openlp.core.lib.os.makedirs') as mocked_makedirs: - # GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists - directory_to_check = 'existing/directory' - - # WHEN: os.path.exists returns True and we check to see if the directory exists - mocked_exists.return_value = True - check_directory_exists(directory_to_check) - - # THEN: Only os.path.exists should have been called - mocked_exists.assert_called_with(directory_to_check) - self.assertIsNot(mocked_makedirs.called, 'os.makedirs should not have been called') - - # WHEN: os.path.exists returns False and we check the directory exists - mocked_exists.return_value = False - check_directory_exists(directory_to_check) - - # THEN: Both the mocked functions should have been called - mocked_exists.assert_called_with(directory_to_check) - mocked_makedirs.assert_called_with(directory_to_check) - - # WHEN: os.path.exists raises an IOError - mocked_exists.side_effect = IOError() - check_directory_exists(directory_to_check) - - # THEN: We shouldn't get an exception though the mocked exists has been called - mocked_exists.assert_called_with(directory_to_check) - - # WHEN: Some other exception is raised - mocked_exists.side_effect = ValueError() - - # THEN: check_directory_exists raises an exception - mocked_exists.assert_called_with(directory_to_check) - self.assertRaises(ValueError, check_directory_exists, directory_to_check) - def get_text_file_string_no_file_test(self): """ Test the get_text_file_string() function when a file does not exist @@ -353,7 +294,7 @@ class TestLib(TestCase): Test that the check_item_selected() function returns True when there are selected indexes """ # GIVEN: A mocked out QtGui module and a list widget with selected indexes - MockedQtGui = patch('openlp.core.lib.QtGui') + mocked_QtGui = patch('openlp.core.lib.QtGui') mocked_list_widget = MagicMock() mocked_list_widget.selectedIndexes.return_value = True message = 'message' @@ -508,6 +449,27 @@ class TestLib(TestCase): mocked_os.stat.assert_any_call(thumb_path) assert result is False, 'The result should be False' + def resize_thumb_test(self): + """ + Test the resize_thumb() function + """ + # GIVEN: A path to an image. + image_path = os.path.join(TEST_PATH, 'church.jpg') + wanted_width = 777 + wanted_height = 72 + # We want the background to be white. + wanted_background_hex = '#FFFFFF' + wanted_background_rgb = QtGui.QColor(wanted_background_hex).rgb() + + # WHEN: Resize the image and add a background. + image = resize_image(image_path, wanted_width, wanted_height, wanted_background_hex) + + # THEN: Check if the size is correct and the background was set. + result_size = image.size() + self.assertEqual(wanted_height, result_size.height(), 'The image should have the requested height.') + self.assertEqual(wanted_width, result_size.width(), 'The image should have the requested width.') + self.assertEqual(image.pixel(0, 0), wanted_background_rgb, 'The background should be white.') + def create_separated_list_qlocate_test(self): """ Test the create_separated_list function using the Qt provided method diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index f1b8ee17a..591762947 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -154,6 +154,21 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) + def test_create_checked_enabled_visible_action(self): + """ + Test creating an action with the 'checked', 'enabled' and 'visible' properties. + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action with some properties + action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) + + # THEN: These properties should be set + self.assertEqual(True, action.isChecked()) + self.assertEqual(False, action.isEnabled()) + self.assertEqual(False, action.isVisible()) + def test_create_valign_selection_widgets(self): """ Test creating a combo box for valign selection @@ -197,3 +212,33 @@ class TestUi(TestCase): # THEN: The index should have changed self.assertEqual(2, combo.currentIndex()) + + def test_create_widget_action(self): + """ + Test creating an action for a widget + """ + # GIVEN: A button + button = QtGui.QPushButton() + + # WHEN: We call the function + action = create_widget_action(button, 'some action') + + # THEN: The action should be returned + self.assertIsInstance(action, QtGui.QAction) + self.assertEqual(action.objectName(), 'some action') + + def test_set_case_insensitive_completer(self): + """ + Test setting a case insensitive completer on a widget + """ + # GIVEN: A QComboBox and a list of completion items + line_edit = QtGui.QLineEdit() + suggestions = ['one', 'Two', 'THRee', 'FOUR'] + + # WHEN: We call the function + set_case_insensitive_completer(suggestions, line_edit) + + # THEN: The Combobox should have a completer which is case insensitive + completer = line_edit.completer() + self.assertIsInstance(completer, QtGui.QCompleter) + self.assertEqual(completer.caseSensitivity(), QtCore.Qt.CaseInsensitive) diff --git a/tests/functional/openlp_core_ui/test_servicemanager.py b/tests/functional/openlp_core_ui/test_servicemanager.py index 260f88b6b..2c76ed965 100644 --- a/tests/functional/openlp_core_ui/test_servicemanager.py +++ b/tests/functional/openlp_core_ui/test_servicemanager.py @@ -71,7 +71,21 @@ class TestServiceManager(TestCase): service_manager._save_lite = False service_manager.service_theme = 'test_theme' service = service_manager.create_basic_service()[0] - # THEN: The the controller should be registered in the registry. + # THEN: The controller should be registered in the registry. self.assertNotEqual(service, None, 'The base service should be created') self.assertEqual(service['openlp_core']['service-theme'], 'test_theme', 'The test theme should be saved') self.assertEqual(service['openlp_core']['lite-service'], False, 'The lite service should be saved') + + def supported_suffixes_test(self): + """ + Test the create basic service array + """ + # GIVEN: A new service manager instance. + service_manager = ServiceManager(None) + # WHEN: a suffix is added as an individual or a list. + service_manager.supported_suffixes('txt') + service_manager.supported_suffixes(['pptx', 'ppt']) + # THEN: The suffixes should be available to test. + self.assertEqual('txt' in service_manager.suffixes, True, 'The suffix txt should be in the list') + self.assertEqual('ppt' in service_manager.suffixes, True, 'The suffix ppt should be in the list') + self.assertEqual('pptx' in service_manager.suffixes, True, 'The suffix pptx should be in the list') diff --git a/tests/functional/openlp_plugins/bibles/test_opensongimport.py b/tests/functional/openlp_plugins/bibles/test_opensongimport.py new file mode 100644 index 000000000..542cf289d --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_opensongimport.py @@ -0,0 +1,140 @@ +# -*- 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 OpenSong Bible importer. +""" + +import os +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.bibles.lib.opensong import OpenSongBible +from openlp.plugins.bibles.lib.db import BibleDB + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) +OPENSONG_TEST_DATA = { + 'opensong-dk1933.xml': { + 'book': 'Genesis', + 'chapter': 1, + 'verses': [ + (1, 'I Begyndelsen skabte Gud Himmelen og Jorden.'), + (2, 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. ' + 'Men Guds Ånd svævede over Vandene.'), + (3, 'Og Gud sagde: "Der blive Lys!" Og der blev Lys.'), + (4, 'Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket,'), + (5, 'og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, ' + 'og det blev Morgen, første Dag.'), + (6, 'Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!"'), + (7, 'Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen ' + 'fra Vandet over Hvælvingen;'), + (8, 'og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.'), + (9, 'Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" ' + 'Og således skete det;'), + (10, 'og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, ' + 'at det var godt.') + ] + } +} + + +class TestOpenSongImport(TestCase): + """ + Test the functions in the :mod:`opensongimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry') + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.manager_patcher.start() + + def tearDown(self): + self.registry_patcher.stop() + self.manager_patcher.stop() + + def create_importer_test(self): + """ + Test creating an instance of the OpenSong file importer + """ + # GIVEN: A mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + + # THEN: The importer should be an instance of BibleDB + self.assertIsInstance(importer, BibleDB) + + def file_import_test(self): + """ + Test the actual import of real song files + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + with patch('openlp.plugins.bibles.lib.opensong.OpenSongBible.application'): + for bible_file in OPENSONG_TEST_DATA: + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in OPENSONG_TEST_DATA[bible_file]['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, 1, verse_tag, verse_text) + + def zefania_import_error_test(self): + """ + Test that we give an error message if trying to import a zefania bible + """ + # GIVEN: A mocked out "manager" and mocked out critical_error_message_box and an import + with patch('openlp.plugins.bibles.lib.opensong.critical_error_message_box') as \ + mocked_critical_error_message_box: + mocked_manager = MagicMock() + importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + + # WHEN: An trying to import a zefania bible + importer.filename = os.path.join(TEST_PATH, 'zefania-dk1933.xml') + importer.do_import() + + # THEN: The importer should have "shown" an error message + mocked_critical_error_message_box.assert_called_with(message='Incorrect Bible file type supplied. ' + 'This looks like a Zefania XML bible, ' + 'please use the Zefania import option.') diff --git a/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py new file mode 100644 index 000000000..50307b401 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py @@ -0,0 +1,121 @@ +# -*- 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 Zefania Bible importer. +""" + +import os +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.bibles.lib.zefania import ZefaniaBible +from openlp.plugins.bibles.lib.db import BibleDB + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) +ZEFANIA_TEST_DATA = { + 'zefania-dk1933.xml': { + 'book': 'Genesis', + 'chapter': 1, + 'verses': [ + ('1', 'I Begyndelsen skabte Gud Himmelen og Jorden.'), + ('2', 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. ' + 'Men Guds Ånd svævede over Vandene.'), + ('3', 'Og Gud sagde: "Der blive Lys!" Og der blev Lys.'), + ('4', 'Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket,'), + ('5', 'og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, ' + 'og det blev Morgen, første Dag.'), + ('6', 'Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!"'), + ('7', 'Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen ' + 'fra Vandet over Hvælvingen;'), + ('8', 'og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.'), + ('9', 'Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" ' + 'Og således skete det;'), + ('10', 'og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, ' + 'at det var godt.') + ] + } +} + + +class TestZefaniaImport(TestCase): + """ + Test the functions in the :mod:`zefaniaimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry') + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.manager_patcher.start() + + def tearDown(self): + self.registry_patcher.stop() + self.manager_patcher.stop() + + def create_importer_test(self): + """ + Test creating an instance of the Zefania file importer + """ + # GIVEN: A mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + + # THEN: The importer should be an instance of BibleDB + self.assertIsInstance(importer, BibleDB) + + def file_import_test(self): + """ + Test the actual import of real song files + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + with patch('openlp.plugins.bibles.lib.zefania.ZefaniaBible.application'): + for bible_file in ZEFANIA_TEST_DATA: + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in ZEFANIA_TEST_DATA[bible_file]['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) diff --git a/tests/functional/openlp_plugins/songs/test_db.py b/tests/functional/openlp_plugins/songs/test_db.py new file mode 100644 index 000000000..3080db77e --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_db.py @@ -0,0 +1,114 @@ +# -*- 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 db submodule of the Songs plugin. +""" +from unittest import TestCase + +from openlp.plugins.songs.lib.db import Song, Author, AuthorType + + +class TestDB(TestCase): + """ + Test the functions in the :mod:`db` module. + """ + + def test_add_author(self): + """ + Test adding an author to a song + """ + # GIVEN: A song and an author + song = Song() + song.authors_songs = [] + author = Author() + author.first_name = "Max" + author.last_name = "Mustermann" + + # WHEN: We add an author to the song + song.add_author(author) + + # THEN: The author should have been added with author_type=None + self.assertEqual(1, len(song.authors_songs)) + self.assertEqual("Max", song.authors_songs[0].author.first_name) + self.assertEqual("Mustermann", song.authors_songs[0].author.last_name) + self.assertIsNone(song.authors_songs[0].author_type) + + def test_add_author_with_type(self): + """ + Test adding an author with a type specified to a song + """ + # GIVEN: A song and an author + song = Song() + song.authors_songs = [] + author = Author() + author.first_name = "Max" + author.last_name = "Mustermann" + + # WHEN: We add an author to the song + song.add_author(author, AuthorType.Words) + + # THEN: The author should have been added with author_type=None + self.assertEqual(1, len(song.authors_songs)) + self.assertEqual("Max", song.authors_songs[0].author.first_name) + self.assertEqual("Mustermann", song.authors_songs[0].author.last_name) + self.assertEqual(AuthorType.Words, song.authors_songs[0].author_type) + + def test_remove_author(self): + """ + Test removing an author from a song + """ + # GIVEN: A song with an author + song = Song() + song.authors_songs = [] + author = Author() + song.add_author(author) + + # WHEN: We remove the author + song.remove_author(author) + + # THEN: It should have been removed + self.assertEqual(0, len(song.authors_songs)) + + def test_remove_author_with_type(self): + """ + Test removing an author with a type specified from a song + """ + # GIVEN: A song with two authors + song = Song() + song.authors_songs = [] + author = Author() + song.add_author(author) + song.add_author(author, AuthorType.Translation) + + # WHEN: We remove the author with a certain type + song.remove_author(author, AuthorType.Translation) + + # THEN: It should have been removed and the other author should still be there + self.assertEqual(1, len(song.authors_songs)) + self.assertEqual(None, song.authors_songs[0].author_type) diff --git a/tests/functional/openlp_plugins/songs/test_ewimport.py b/tests/functional/openlp_plugins/songs/test_ewimport.py index 8d9302015..ea557711b 100644 --- a/tests/functional/openlp_plugins/songs/test_ewimport.py +++ b/tests/functional/openlp_plugins/songs/test_ewimport.py @@ -314,6 +314,26 @@ class TestEasyWorshipSongImport(TestCase): mocked_os_path.isfile.assert_any_call('Songs.DB') mocked_os_path.isfile.assert_any_call('Songs.MB') + def do_import_source_invalid_test(self): + """ + Test the :mod:`do_import` module produces an error when Songs.MB not found. + """ + # GIVEN: A mocked out SongImport class, a mocked out "manager" + with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \ + patch('openlp.plugins.songs.lib.ewimport.os.path') as mocked_os_path: + mocked_manager = MagicMock() + importer = EasyWorshipSongImport(mocked_manager, filenames=[]) + importer.log_error = MagicMock() + mocked_os_path.isfile.side_effect = [True, False] + + # WHEN: do_import is supplied with an import source (Songs.MB missing) + importer.import_source = 'Songs.DB' + importer.do_import() + + # THEN: do_import should have logged an error that the Songs.MB file could not be found. + importer.log_error.assert_any_call(importer.import_source, 'Could not find the "Songs.MB" file. It must be ' + 'in the same folder as the "Songs.DB" file.') + def do_import_database_validity_test(self): """ Test the :mod:`do_import` module handles invalid database files correctly diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 22291c6a6..bc22a4577 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -153,3 +153,52 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The songbook should be in the footer self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12']) + + def authors_match_test(self): + """ + Test the author matching when importing a song from a service + """ + # GIVEN: A song and a string with authors + song = MagicMock() + song.authors = [] + author = MagicMock() + author.display_name = "Hans Wurst" + song.authors.append(author) + author2 = MagicMock() + author2.display_name = "Max Mustermann" + song.authors.append(author2) + # There are occasions where an author appears twice in a song (with different types). + # We need to make sure that this case works (lp#1313538) + author3 = MagicMock() + author3.display_name = "Max Mustermann" + song.authors.append(author3) + authors_str = "Hans Wurst, Max Mustermann, Max Mustermann" + + # WHEN: Checking for matching + result = self.media_item._authors_match(song, authors_str) + + # THEN: They should match + self.assertTrue(result, "Authors should match") + + def authors_dont_match_test(self): + # GIVEN: A song and a string with authors + song = MagicMock() + song.authors = [] + author = MagicMock() + author.display_name = "Hans Wurst" + song.authors.append(author) + author2 = MagicMock() + author2.display_name = "Max Mustermann" + song.authors.append(author2) + # There are occasions where an author appears twice in a song (with different types). + # We need to make sure that this case works (lp#1313538) + author3 = MagicMock() + author3.display_name = "Max Mustermann" + song.authors.append(author3) + + # WHEN: An author is missing in the string + authors_str = "Hans Wurst, Max Mustermann" + result = self.media_item._authors_match(song, authors_str) + + # THEN: They should not match + self.assertFalse(result, "Authors should not match") diff --git a/tests/functional/openlp_plugins/songs/test_opensongimport.py b/tests/functional/openlp_plugins/songs/test_opensongimport.py new file mode 100644 index 000000000..70d3b342a --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_opensongimport.py @@ -0,0 +1,121 @@ +# -*- 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 OpenSong song importer. +""" + +import os +from unittest import TestCase + +from tests.helpers.songfileimport import SongImportTestHelper +from openlp.plugins.songs.lib.opensongimport import OpenSongImport +from tests.functional import patch, MagicMock + +TEST_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'opensongsongs')) + + +class TestOpenSongFileImport(SongImportTestHelper): + + def __init__(self, *args, **kwargs): + self.importer_class_name = 'OpenSongImport' + self.importer_module_name = 'opensongimport' + super(TestOpenSongFileImport, self).__init__(*args, **kwargs) + + def test_song_import(self): + """ + Test that loading an OpenSong file works correctly on various files + """ + self.file_import(os.path.join(TEST_PATH, 'Amazing Grace'), + 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'), + self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json'))) + self.file_import(os.path.join(TEST_PATH, 'One, Two, Three, Four, Five'), + self.load_external_result_data(os.path.join(TEST_PATH, 'One, Two, Three, Four, Five.json'))) + + +class TestOpenSongImport(TestCase): + """ + Test the functions in the :mod:`opensongimport` module. + """ + def create_importer_test(self): + """ + Test creating an instance of the OpenSong file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.opensongimport.SongImport'): + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OpenSongImport(mocked_manager, filenames=[]) + + # THEN: The importer object should not be None + self.assertIsNotNone(importer, 'Import should not be none') + + def invalid_import_source_test(self): + """ + Test OpenSongImport.do_import handles different invalid import_source values + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.opensongimport.SongImport'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenSongImport(mocked_manager, filenames=[]) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is not a list + for source in ['not a list', 0]: + importer.import_source = source + + # THEN: do_import should return none and the progress bar maximum should not be set. + self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list') + self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False, + 'setMaximum on import_wizard.progress_bar should not have been called') + + def valid_import_source_test(self): + """ + Test OpenSongImport.do_import handles different invalid import_source values + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + with patch('openlp.plugins.songs.lib.opensongimport.SongImport'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenSongImport(mocked_manager, filenames=[]) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is a list + importer.import_source = ['List', 'of', 'files'] + + # THEN: do_import should return none and the progress bar setMaximum should be called with the length of + # import_source. + self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is a list ' + 'and stop_import_flag is True') + mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source)) diff --git a/tests/functional/openlp_plugins/songs/test_propresenterimport.py b/tests/functional/openlp_plugins/songs/test_propresenterimport.py new file mode 100644 index 000000000..971ddbdf6 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_propresenterimport.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 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:`propresenterimport` module provides the functionality for importing +ProPresenter song files into the current installation database. +""" + +import os + +from tests.helpers.songfileimport import SongImportTestHelper + +TEST_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'propresentersongs')) + + +class TestProPresenterFileImport(SongImportTestHelper): + + def __init__(self, *args, **kwargs): + self.importer_class_name = 'ProPresenterImport' + self.importer_module_name = 'propresenterimport' + super(TestProPresenterFileImport, self).__init__(*args, **kwargs) + + def test_song_import(self): + """ + Test that loading an ProPresenter file works correctly + """ + self.file_import(os.path.join(TEST_PATH, 'Amazing Grace.pro4'), + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) diff --git a/tests/functional/openlp_plugins/songs/test_songselect.py b/tests/functional/openlp_plugins/songs/test_songselect.py index 8d1237190..0f9001342 100644 --- a/tests/functional/openlp_plugins/songs/test_songselect.py +++ b/tests/functional/openlp_plugins/songs/test_songselect.py @@ -344,7 +344,7 @@ class TestSongSelect(TestCase): 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') + self.assertEqual(1, len(result.authors_songs), 'There should only be one author') def save_song_existing_author_test(self): """ @@ -379,4 +379,4 @@ class TestSongSelect(TestCase): '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') + self.assertEqual(1, len(result.authors_songs), 'There should only be one author') diff --git a/tests/functional/openlp_plugins/songs/test_zionworximport.py b/tests/functional/openlp_plugins/songs/test_zionworximport.py index 2edc071c7..c5669e9c8 100644 --- a/tests/functional/openlp_plugins/songs/test_zionworximport.py +++ b/tests/functional/openlp_plugins/songs/test_zionworximport.py @@ -46,7 +46,7 @@ class TestZionWorxImport(TestCase): Test creating an instance of the ZionWorx file importer """ # GIVEN: A mocked out SongImport class, and a mocked out "manager" - with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'): + with patch('openlp.plugins.songs.lib.zionworximport.SongImport'): mocked_manager = MagicMock() # WHEN: An importer object is created diff --git a/tests/helpers/songfileimport.py b/tests/helpers/songfileimport.py index 5364c2c3b..36beef6e5 100644 --- a/tests/helpers/songfileimport.py +++ b/tests/helpers/songfileimport.py @@ -33,7 +33,7 @@ song files from third party applications. import json from unittest import TestCase -from tests.functional import patch, MagicMock +from tests.functional import patch, MagicMock, call class SongImportTestHelper(TestCase): @@ -56,13 +56,13 @@ class SongImportTestHelper(TestCase): 'openlp.plugins.songs.lib.%s.%s.add_verse' % (self.importer_module_name, self.importer_class_name)) self.finish_patcher = patch( 'openlp.plugins.songs.lib.%s.%s.finish' % (self.importer_module_name, self.importer_class_name)) - self.parse_author_patcher = patch( - 'openlp.plugins.songs.lib.%s.%s.parse_author' % (self.importer_module_name, self.importer_class_name)) + self.add_author_patcher = patch( + 'openlp.plugins.songs.lib.%s.%s.add_author' % (self.importer_module_name, self.importer_class_name)) self.song_import_patcher = patch('openlp.plugins.songs.lib.%s.SongImport' % self.importer_module_name) self.mocked_add_copyright = self.add_copyright_patcher.start() self.mocked_add_verse = self.add_verse_patcher.start() self.mocked_finish = self.finish_patcher.start() - self.mocked_parse_author = self.parse_author_patcher.start() + self.mocked_add_author = self.add_author_patcher.start() self.mocked_song_importer = self.song_import_patcher.start() self.mocked_manager = MagicMock() self.mocked_import_wizard = MagicMock() @@ -75,7 +75,7 @@ class SongImportTestHelper(TestCase): self.add_copyright_patcher.stop() self.add_verse_patcher.stop() self.finish_patcher.stop() - self.parse_author_patcher.stop() + self.add_author_patcher.stop() self.song_import_patcher.stop() def load_external_result_data(self, file_name): @@ -112,14 +112,17 @@ class SongImportTestHelper(TestCase): self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed') self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title)) for author in author_calls: - self.mocked_parse_author.assert_any_call(author) + self.mocked_add_author.assert_any_call(author) if song_copyright: self.mocked_add_copyright.assert_called_with(song_copyright) if ccli_number: self.assertEqual(importer.ccli_number, ccli_number, 'ccli_number for %s should be %s' % (source_file_name, ccli_number)) + expected_calls = [] for verse_text, verse_tag in add_verse_calls: self.mocked_add_verse.assert_any_call(verse_text, verse_tag) + expected_calls.append(call(verse_text, verse_tag)) + self.mocked_add_verse.assert_has_calls(expected_calls, any_order=False) if topics: self.assertEqual(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics)) if comments: @@ -132,7 +135,7 @@ class SongImportTestHelper(TestCase): self.assertEqual(importer.song_number, song_number, 'song_number for %s should be %s' % (source_file_name, song_number)) if verse_order_list: - self.assertEqual(importer.verse_order_list, [], + self.assertEqual(importer.verse_order_list, verse_order_list, 'verse_order_list for %s should be %s' % (source_file_name, verse_order_list)) self.mocked_finish.assert_called_with() diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core_lib/test_pluginmanager.py index 262f9f2f3..ba81d708f 100644 --- a/tests/interfaces/openlp_core_lib/test_pluginmanager.py +++ b/tests/interfaces/openlp_core_lib/test_pluginmanager.py @@ -88,7 +88,7 @@ class TestPluginManager(TestCase, TestMixin): plugin_names = [plugin.name for plugin in plugin_manager.plugins] assert 'songs' in plugin_names, 'There should be a "songs" plugin.' assert 'bibles' in plugin_names, 'There should be a "bibles" plugin.' - assert 'presentations' not in plugin_names, 'There should NOT be a "presentations" plugin.' + assert 'presentations' in plugin_names, 'There should be a "presentations" plugin.' assert 'images' in plugin_names, 'There should be a "images" plugin.' assert 'media' in plugin_names, 'There should be a "media" plugin.' assert 'custom' in plugin_names, 'There should be a "custom" plugin.' diff --git a/tests/resources/bibles/opensong-dk1933.xml b/tests/resources/bibles/opensong-dk1933.xml new file mode 100644 index 000000000..1aa1140f7 --- /dev/null +++ b/tests/resources/bibles/opensong-dk1933.xml @@ -0,0 +1,46 @@ + + + + + + + + + Danish Version + + + The Holy Bible + The electronic edition of this Bible comes from the Danish 1933 edition. The Old Testament is an update from the 1931 edition, and the New Testament is an update from the 1907 edition. + Free Bible Software Group + + The Unbound Bible + Biola University: Administrative Computing + 13800 Biola Ave. + La Mirada, CA 90639 + United States of America + 562-903-4722 + + 2009-01-20 + Zefania XML Bible Markup Language + DAN + ftp://unboundftp.biola.edu/pub/danish1933_utf8.zip + DAN + provide the bible to the world + + + + + + I Begyndelsen skabte Gud Himmelen og Jorden. + Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene. + Og Gud sagde: "Der blive Lys!" Og der blev Lys. + Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket, + og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag. + Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!" + Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen; + og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag. + Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" Og således skete det; + og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt. + + + diff --git a/tests/resources/bibles/zefania-dk1933.xml b/tests/resources/bibles/zefania-dk1933.xml new file mode 100644 index 000000000..f538edb99 --- /dev/null +++ b/tests/resources/bibles/zefania-dk1933.xml @@ -0,0 +1,45 @@ + + + + + + + + Danish Version + + + The Holy Bible + The electronic edition of this Bible comes from the Danish 1933 edition. The Old Testament is an update from the 1931 edition, and the New Testament is an update from the 1907 edition. + Free Bible Software Group + + The Unbound Bible + Biola University: Administrative Computing + 13800 Biola Ave. + La Mirada, CA 90639 + United States of America + 562-903-4722 + + 2009-01-20 + Zefania XML Bible Markup Language + DAN + ftp://unboundftp.biola.edu/pub/danish1933_utf8.zip + DAN + provide the bible to the world + + + + + + I skabte Gud Himmelen og Jorden. + Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene. + Og Gud sagde: "Der blive Lys!" Og der blev Lys. + Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket, + og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag. + Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!" + Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen; + og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag. + Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" Og således skete det; + og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt. + + + \ No newline at end of file diff --git a/tests/resources/opensongsongs/Amazing Grace b/tests/resources/opensongsongs/Amazing Grace new file mode 100644 index 000000000..97062dc21 --- /dev/null +++ b/tests/resources/opensongsongs/Amazing Grace @@ -0,0 +1,56 @@ + + + Amazing Grace (Demonstration) + John Newton, Edwin Excell & John P. Rees + Public Domain + V1 V2 V3 V4 V5 + + + 22025 + God: Assurance/Grace/Salvation + Worship: Praise + + + + [V] +;Test the chords format +;Chords beging with . +;Verses begin with their verse number +;Link words with _ +;Comments begin with ; +. D D7 G D +1A______ma________zing grace! How sweet the sound! +2'Twas grace that taught my heart to fear, +3The Lord has pro____mised good to me, +4Thro' ma________ny dan____gers, toils and snares +5When we've been there ten thou__sand years, + +. Bm E A A7 +1That saved a wretch like me! +2And grace my fears re___lieved. +3His Word my hope se___cures. +4I have al___rea____dy come. +5Bright shi___ning as the sun, + +. D D7 G D +1I once was lost, but now am found; +2How pre___cious did that grace ap____pear, +3He will my shield and por___tion be +4'Tis grace that brought me safe thus far, +5We've no less days to sing God's praise, + +. Bm A G D +1Was blind, but now I see. +2The hour I first be_lieved. +3As long as life en_dures. +4And grace will lead me home. +5Than when we first be_gun. + + + Demonstration Songs 0 + + + + + + \ No newline at end of file diff --git a/tests/resources/opensongsongs/Amazing Grace.json b/tests/resources/opensongsongs/Amazing Grace.json new file mode 100644 index 000000000..97b8c77b7 --- /dev/null +++ b/tests/resources/opensongsongs/Amazing Grace.json @@ -0,0 +1,42 @@ +{ + "authors": [ + "John Newton", + "Edwin Excell", + "John P. Rees" + ], + "ccli_number": 22025, + "comments": "\n\n\n", + "copyright": "Public Domain ", + "song_book_name": "Demonstration Songs", + "song_number": 0, + "title": "Amazing Grace (Demonstration)", + "topics": [ + "Assurance", + "Grace", + "Praise", + "Salvation" + ], + "verse_order_list": [], + "verses": [ + [ + "Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.", + "v1" + ], + [ + "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.", + "v2" + ], + [ + "The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.", + "v3" + ], + [ + "Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.", + "v4" + ], + [ + "When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.", + "v5" + ] + ] +} \ No newline at end of file diff --git a/tests/resources/opensongsongs/Beautiful Garden Of Prayer b/tests/resources/opensongsongs/Beautiful Garden Of Prayer new file mode 100644 index 000000000..29cd26cc8 --- /dev/null +++ b/tests/resources/opensongsongs/Beautiful Garden Of Prayer @@ -0,0 +1,56 @@ + + + Beautiful Garden Of Prayer (Demonstration) + Eleanor Allen Schroll & James H. Fillmore + Public Domain + V1 C V2 C V3 C + + + 60252 + God: Prayer/Devotion + Prayer: Prayer/Devotion + + + + +;Test breaks and newlines +;A single | on the end of a line adds an extra \n +;Blank lines are ignored, even with a space prefix +;We also check that the chorus is added after the verses, despite the order in the file +[V1] + There's a garden where Jesus is waiting, + + There's a place that is wondrously fair. + For it glows with the light of His presence,| + 'Tis the beautiful garden of prayer. + +;A double || on a line adds a new slide +[C] + O the beautiful garden, the garden of prayer, + O the beautiful garden of prayer. + There my Savior awaits, and He opens the gates + || + To the beautiful garden of prayer. + +;A double || on the end of a line adds a new slide +[V2] + There's a garden where Jesus is waiting, + And I go with my burden and care. + Just to learn from His lips, words of comfort,|| + In the beautiful garden of prayer. + +;A single | on a line adds just one line break +[V3] + There's a garden where Jesus is waiting, + And He bids you to come meet Him there, + Just to bow and receive a new blessing, + | + In the beautiful garden of prayer. + + DS0 + + + + + + \ No newline at end of file diff --git a/tests/resources/opensongsongs/Beautiful Garden Of Prayer.json b/tests/resources/opensongsongs/Beautiful Garden Of Prayer.json new file mode 100644 index 000000000..392bbaa18 --- /dev/null +++ b/tests/resources/opensongsongs/Beautiful Garden Of Prayer.json @@ -0,0 +1,35 @@ +{ + "authors": [ + "Eleanor Allen Schroll", + "James H. Fillmore" + ], + "ccli_number": 60252, + "comments": "", + "copyright": "Public Domain ", + "song_book_name": "DS", + "song_number": 0, + "title": "Beautiful Garden Of Prayer (Demonstration)", + "topics": [ + "Devotion", + "Prayer" + ], + "verse_order_list": ["v1", "c1", "v2", "c1", "v3", "c1"], + "verses": [ + [ + "There's a garden where Jesus is waiting,\nThere's a place that is wondrously fair.\nFor it glows with the light of His presence,\n\n'Tis the beautiful garden of prayer.", + "v1" + ], + [ + "There's a garden where Jesus is waiting,\nAnd I go with my burden and care.\nJust to learn from His lips, words of comfort,\n[---]\nIn the beautiful garden of prayer.", + "v2" + ], + [ + "There's a garden where Jesus is waiting,\nAnd He bids you to come meet Him there,\nJust to bow and receive a new blessing,\n\nIn the beautiful garden of prayer.", + "v3" + ], + [ + "O the beautiful garden, the garden of prayer,\nO the beautiful garden of prayer.\nThere my Savior awaits, and He opens the gates\n[---]\nTo the beautiful garden of prayer.", + "c1" + ] + ] +} \ No newline at end of file diff --git a/tests/resources/opensongsongs/One, Two, Three, Four, Five b/tests/resources/opensongsongs/One, Two, Three, Four, Five new file mode 100644 index 000000000..cc6bc107f --- /dev/null +++ b/tests/resources/opensongsongs/One, Two, Three, Four, Five @@ -0,0 +1,38 @@ + + + 12345 + Traditional + Public Domain + T + + + + + + + + + +;Test [T]ag element - should be turned into [o]ther +;And lines beginning with numbers +;And a title that contains only numeric characters +;That isdiffernt to the filename +;And most elements are empty +[T] + 1, 2, 3, 4, 5, + Once I caught a fish alive. + 6, 7, 8, 9, 10, + Then I let it go again. + + Why did you let it go? + Because it bit my finger so. + Which finger did it bite? + This little finger on my right. + + + + + + + + \ No newline at end of file diff --git a/tests/resources/opensongsongs/One, Two, Three, Four, Five.json b/tests/resources/opensongsongs/One, Two, Three, Four, Five.json new file mode 100644 index 000000000..30fe71c64 --- /dev/null +++ b/tests/resources/opensongsongs/One, Two, Three, Four, Five.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "Traditional" + ], + "comments": "", + "copyright": "Public Domain ", + "title": "12345", + "topics": [ + ], + "verse_order_list": ["o1"], + "verses": [ + [ + "1, 2, 3, 4, 5,\nOnce I caught a fish alive.\n6, 7, 8, 9, 10,\nThen I let it go again.\nWhy did you let it go?\nBecause it bit my finger so.\nWhich finger did it bite?\nThis little finger on my right.", + "o1" + ] + ] +} \ No newline at end of file diff --git a/tests/resources/propresentersongs/Amazing Grace.json b/tests/resources/propresentersongs/Amazing Grace.json new file mode 100644 index 000000000..1746b7696 --- /dev/null +++ b/tests/resources/propresentersongs/Amazing Grace.json @@ -0,0 +1,121 @@ +{ + "authors": [ + "John Newton" + ], + "title": "Amazing Grace", + "verse_order_list": [], + "verses": [ + [ + "Amazing grace! How sweet the sound\n", + "v1" + ], + [ + "That saved a wretch like me!\n", + "v2" + ], + [ + "I once was lost, but now am found;\n", + "v3" + ], + [ + "Was blind, but now I see.\n", + "v4" + ], + [ + "'Twas grace that taught my heart to fear,\n", + "v5" + ], + [ + "And grace my fears relieved;\n", + "v6" + ], + [ + "How precious did that grace appear\n", + "v7" + ], + [ + "The hour I first believed.\n", + "v8" + ], + [ + "Through many dangers, toils and snares,\n", + "v9" + ], + [ + "I have already come;\n", + "v10" + ], + [ + "'Tis grace hath brought me safe thus far,\n", + "v11" + ], + [ + "And grace will lead me home.\n", + "v12" + ], + [ + "The Lord has promised good to me,\n", + "v13" + ], + [ + "His Word my hope secures;\n", + "v14" + ], + [ + "He will my Shield and Portion be,\n", + "v15" + ], + [ + "As long as life endures.\n", + "v16" + ], + [ + "Yea, when this flesh and heart shall fail,\n", + "v17" + ], + [ + "And mortal life shall cease,\n", + "v18" + ], + [ + "I shall possess, within the veil,\n", + "v19" + ], + [ + "A life of joy and peace.\n", + "v20" + ], + [ + "The earth shall soon dissolve like snow,\n", + "v21" + ], + [ + "The sun forbear to shine;\n", + "v22" + ], + [ + "But God, Who called me here below,\n", + "v23" + ], + [ + "Shall be forever mine.\n", + "v24" + ], + [ + "When we've been there ten thousand years,\n", + "v25" + ], + [ + "Bright shining as the sun,\n", + "v26" + ], + [ + "We've no less days to sing God's praise\n", + "v27" + ], + [ + "Than when we'd first begun.\n", + "v28" + ] + ] +} \ No newline at end of file diff --git a/tests/resources/propresentersongs/Amazing Grace.pro4 b/tests/resources/propresentersongs/Amazing Grace.pro4 new file mode 100644 index 000000000..dbe114a88 --- /dev/null +++ b/tests/resources/propresentersongs/Amazing Grace.pro4 @@ -0,0 +1,486 @@ + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + + + + + + + + \ No newline at end of file