← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/pysword-import into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/pysword-import into lp:openlp.

Requested reviews:
  Raoul Snyman (raoul-snyman)
Related bugs:
  Bug #785625 in OpenLP: "add downloading Bibles from Sword Project"
  https://bugs.launchpad.net/openlp/+bug/785625

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/pysword-import/+merge/292713

Added support for importing SWORD bibles using PySword.
-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== added file 'openlp/core/ui/wizard.py.OTHER'
--- openlp/core/ui/wizard.py.OTHER	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/wizard.py.OTHER	2016-04-23 18:44:10 +0000
@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# 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:``wizard`` module provides generic wizard tools for OpenLP.
+"""
+import logging
+import os
+
+from PyQt5 import QtGui, QtWidgets
+
+from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx
+from openlp.core.lib import build_icon
+from openlp.core.lib.ui import add_welcome_page
+
+log = logging.getLogger(__name__)
+
+
+class WizardStrings(object):
+    """
+    Provide standard strings for wizards to use.
+    """
+    # Applications/Formats we import from or export to. These get used in
+    # multiple places but do not need translating unless you find evidence of
+    # the writers translating their own product name.
+    CSV = 'CSV'
+    OS = 'OpenSong'
+    OSIS = 'OSIS'
+    ZEF = 'Zefania'
+    SWORD = 'Sword'
+    # These strings should need a good reason to be retranslated elsewhere.
+    FinishedImport = translate('OpenLP.Ui', 'Finished import.')
+    FormatLabel = translate('OpenLP.Ui', 'Format:')
+    HeaderStyle = '<span style="font-size:14pt; font-weight:600;">%s</span>'
+    Importing = translate('OpenLP.Ui', 'Importing')
+    ImportingType = translate('OpenLP.Ui', 'Importing "%s"...')
+    ImportSelect = translate('OpenLP.Ui', 'Select Import Source')
+    ImportSelectLong = translate('OpenLP.Ui', 'Select the import format and the location to import from.')
+    OpenTypeFile = translate('OpenLP.Ui', 'Open %s File')
+    OpenTypeFolder = translate('OpenLP.Ui', 'Open %s Folder')
+    PercentSymbolFormat = translate('OpenLP.Ui', '%p%')
+    Ready = translate('OpenLP.Ui', 'Ready.')
+    StartingImport = translate('OpenLP.Ui', 'Starting import...')
+    YouSpecifyFile = translate('OpenLP.Ui', 'You need to specify one %s file to import from.',
+                               'A file type e.g. OpenSong')
+    YouSpecifyFiles = translate('OpenLP.Ui', 'You need to specify at least one %s file to import from.',
+                                'A file type e.g. OpenSong')
+    YouSpecifyFolder = translate('OpenLP.Ui', 'You need to specify one %s folder to import from.',
+                                 'A song format e.g. PowerSong')
+
+
+class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
+    """
+    Generic OpenLP wizard to provide generic functionality and a unified look
+    and feel.
+
+    ``parent``
+        The QWidget-derived parent of the wizard.
+
+    ``plugin``
+        Plugin this wizard is part of. The plugin will be saved in the "plugin" variable.
+        The plugin will also be used as basis for the file dialog methods this class provides.
+
+    ``name``
+        The object name this wizard should have.
+
+    ``image``
+        The image to display on the "welcome" page of the wizard. Should be 163x350.
+
+    ``add_progress_page``
+        Whether to add a progress page with a progressbar at the end of the wizard.
+    """
+    def __init__(self, parent, plugin, name, image, add_progress_page=True):
+        """
+        Constructor
+        """
+        super(OpenLPWizard, self).__init__(parent)
+        self.plugin = plugin
+        self.with_progress_page = add_progress_page
+        self.setObjectName(name)
+        self.open_icon = build_icon(':/general/general_open.png')
+        self.delete_icon = build_icon(':/general/general_delete.png')
+        self.finish_button = self.button(QtWidgets.QWizard.FinishButton)
+        self.cancel_button = self.button(QtWidgets.QWizard.CancelButton)
+        self.setupUi(image)
+        self.register_fields()
+        self.custom_init()
+        self.custom_signals()
+        self.currentIdChanged.connect(self.on_current_id_changed)
+        if self.with_progress_page:
+            self.error_copy_to_button.clicked.connect(self.on_error_copy_to_button_clicked)
+            self.error_save_to_button.clicked.connect(self.on_error_save_to_button_clicked)
+
+    def setupUi(self, image):
+        """
+        Set up the wizard UI.
+        """
+        self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
+        self.setModal(True)
+        self.setOptions(QtWidgets.QWizard.IndependentPages |
+                        QtWidgets.QWizard.NoBackButtonOnStartPage | QtWidgets.QWizard.NoBackButtonOnLastPage)
+        if is_macosx():
+            self.setPixmap(QtWidgets.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
+        else:
+            self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
+        add_welcome_page(self, image)
+        self.add_custom_pages()
+        if self.with_progress_page:
+            self.add_progress_page()
+        self.retranslateUi()
+
+    def register_fields(self):
+        """
+        Hook method for wizards to register any fields they need.
+        """
+        pass
+
+    def custom_init(self):
+        """
+        Hook method for custom initialisation
+        """
+        pass
+
+    def custom_signals(self):
+        """
+        Hook method for adding custom signals
+        """
+        pass
+
+    def add_custom_pages(self):
+        """
+        Hook method for wizards to add extra pages
+        """
+        pass
+
+    def add_progress_page(self):
+        """
+        Add the progress page for the wizard. This page informs the user how
+        the wizard is progressing with its task.
+        """
+        self.progress_page = QtWidgets.QWizardPage()
+        self.progress_page.setObjectName('progress_page')
+        self.progress_layout = QtWidgets.QVBoxLayout(self.progress_page)
+        self.progress_layout.setContentsMargins(48, 48, 48, 48)
+        self.progress_layout.setObjectName('progress_layout')
+        self.progress_label = QtWidgets.QLabel(self.progress_page)
+        self.progress_label.setObjectName('progress_label')
+        self.progress_label.setWordWrap(True)
+        self.progress_layout.addWidget(self.progress_label)
+        self.progress_bar = QtWidgets.QProgressBar(self.progress_page)
+        self.progress_bar.setObjectName('progress_bar')
+        self.progress_layout.addWidget(self.progress_bar)
+        # Add a QTextEdit and a copy to file and copy to clipboard button to be
+        # able to provide feedback to the user. Hidden by default.
+        self.error_report_text_edit = QtWidgets.QTextEdit(self.progress_page)
+        self.error_report_text_edit.setObjectName('error_report_text_edit')
+        self.error_report_text_edit.setHidden(True)
+        self.error_report_text_edit.setReadOnly(True)
+        self.progress_layout.addWidget(self.error_report_text_edit)
+        self.error_button_layout = QtWidgets.QHBoxLayout()
+        self.error_button_layout.setObjectName('error_button_layout')
+        spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.error_button_layout.addItem(spacer)
+        self.error_copy_to_button = QtWidgets.QPushButton(self.progress_page)
+        self.error_copy_to_button.setObjectName('error_copy_to_button')
+        self.error_copy_to_button.setHidden(True)
+        self.error_copy_to_button.setIcon(build_icon(':/system/system_edit_copy.png'))
+        self.error_button_layout.addWidget(self.error_copy_to_button)
+        self.error_save_to_button = QtWidgets.QPushButton(self.progress_page)
+        self.error_save_to_button.setObjectName('error_save_to_button')
+        self.error_save_to_button.setHidden(True)
+        self.error_save_to_button.setIcon(build_icon(':/general/general_save.png'))
+        self.error_button_layout.addWidget(self.error_save_to_button)
+        self.progress_layout.addLayout(self.error_button_layout)
+        self.addPage(self.progress_page)
+
+    def exec(self):
+        """
+        Run the wizard.
+        """
+        self.set_defaults()
+        return QtWidgets.QWizard.exec(self)
+
+    def reject(self):
+        """
+        Stop the wizard on cancel button, close button or ESC key.
+        """
+        log.debug('Wizard cancelled by user.')
+        if self.with_progress_page and self.currentPage() == self.progress_page:
+            Registry().execute('openlp_stop_wizard')
+        self.done(QtWidgets.QDialog.Rejected)
+
+    def on_current_id_changed(self, page_id):
+        """
+        Perform necessary functions depending on which wizard page is active.
+        """
+        if self.with_progress_page and self.page(page_id) == self.progress_page:
+            self.pre_wizard()
+            self.perform_wizard()
+            self.post_wizard()
+        else:
+            self.custom_page_changed(page_id)
+
+    def custom_page_changed(self, page_id):
+        """
+        Called when changing to a page other than the progress page
+        """
+        pass
+
+    def on_error_copy_to_button_clicked(self):
+        """
+        Called when the ``error_copy_to_button`` has been clicked.
+        """
+        pass
+
+    def on_error_save_to_button_clicked(self):
+        """
+        Called when the ``error_save_to_button`` has been clicked.
+        """
+        pass
+
+    def increment_progress_bar(self, status_text, increment=1):
+        """
+        Update the wizard progress page.
+
+        :param status_text: Current status information to display.
+        :param increment: The value to increment the progress bar by.
+        """
+        log.debug('IncrementBar %s', status_text)
+        self.progress_label.setText(status_text)
+        if increment > 0:
+            self.progress_bar.setValue(self.progress_bar.value() + increment)
+        self.application.process_events()
+
+    def pre_wizard(self):
+        """
+        Prepare the UI for the import.
+        """
+        self.finish_button.setVisible(False)
+        self.progress_bar.setMinimum(0)
+        self.progress_bar.setMaximum(1188)
+        self.progress_bar.setValue(0)
+
+    def post_wizard(self):
+        """
+        Clean up the UI after the import has finished.
+        """
+        self.progress_bar.setValue(self.progress_bar.maximum())
+        self.finish_button.setVisible(True)
+        self.cancel_button.setVisible(False)
+        self.application.process_events()
+
+    def get_file_name(self, title, editbox, setting_name, filters=''):
+        """
+        Opens a QFileDialog and saves the filename to the given editbox.
+
+        :param title: The title of the dialog (unicode).
+        :param editbox:  An editbox (QLineEdit).
+        :param setting_name: The place where to save the last opened directory.
+        :param filters: The file extension filters. It should contain the file description
+            as well as the file extension. For example::
+
+                'OpenLP 2 Databases (*.sqlite)'
+        """
+        if filters:
+            filters += ';;'
+        filters += '%s (*)' % UiStrings().AllFiles
+        filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
+            self, title, os.path.dirname(Settings().value(self.plugin.settings_section + '/' + setting_name)),
+            filters)
+        if filename:
+            editbox.setText(filename)
+        Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename)
+
+    def get_folder(self, title, editbox, setting_name):
+        """
+        Opens a QFileDialog and saves the selected folder to the given editbox.
+
+        :param title: The title of the dialog (unicode).
+        :param editbox: An editbox (QLineEdit).
+        :param setting_name: The place where to save the last opened directory.
+        """
+        folder = QtWidgets.QFileDialog.getExistingDirectory(
+            self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
+            QtWidgets.QFileDialog.ShowDirsOnly)
+        if folder:
+            editbox.setText(folder)
+        Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder)

=== modified file 'openlp/plugins/bibles/forms/bibleimportform.py'
--- openlp/plugins/bibles/forms/bibleimportform.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/bibles/forms/bibleimportform.py	2016-04-23 18:44:10 +0000
@@ -27,6 +27,11 @@
 import urllib.error
 
 from PyQt5 import QtWidgets
+try:
+    from pysword import modules
+    PYSWORD_AVAILABLE = True
+except:
+    PYSWORD_AVAILABLE = False
 
 from openlp.core.common import AppLocation, Settings, UiStrings, translate, clean_filename
 from openlp.core.lib.db import delete_database
@@ -34,7 +39,7 @@
 from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
 from openlp.core.common.languagemanager import get_locale_key
 from openlp.plugins.bibles.lib.manager import BibleFormat
-from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename
+from openlp.plugins.bibles.lib.db import clean_filename
 from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract
 
 log = logging.getLogger(__name__)
@@ -94,6 +99,19 @@
         self.manager.set_process_dialog(self)
         self.restart()
         self.select_stack.setCurrentIndex(0)
+        if PYSWORD_AVAILABLE:
+            self.pysword_folder_modules = modules.SwordModules()
+            try:
+                self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules()
+            except FileNotFoundError:
+                log.debug('No installed SWORD modules found in the default location')
+                self.sword_bible_combo_box.clear()
+                return
+            bible_keys = self.pysword_folder_modules_json.keys()
+            for key in bible_keys:
+                self.sword_bible_combo_box.addItem(self.pysword_folder_modules_json[key]['description'], key)
+        else:
+            self.sword_tab_widget.setDisabled(True)
 
     def custom_signals(self):
         """
@@ -106,6 +124,8 @@
         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)
         self.web_update_button.clicked.connect(self.on_web_update_button_clicked)
+        self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked)
+        self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked)
 
     def add_custom_pages(self):
         """
@@ -121,7 +141,7 @@
         self.format_label = QtWidgets.QLabel(self.select_page)
         self.format_label.setObjectName('FormatLabel')
         self.format_combo_box = QtWidgets.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 = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
@@ -275,6 +295,64 @@
         self.zefania_layout.addRow(self.zefania_file_label, self.zefania_file_layout)
         self.zefania_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer)
         self.select_stack.addWidget(self.zefania_widget)
+        self.sword_widget = QtWidgets.QWidget(self.select_page)
+        self.sword_widget.setObjectName('SwordWidget')
+        self.sword_layout = QtWidgets.QVBoxLayout(self.sword_widget)
+        self.sword_layout.setObjectName('SwordLayout')
+        self.sword_tab_widget = QtWidgets.QTabWidget(self.sword_widget)
+        self.sword_tab_widget.setObjectName('SwordTabWidget')
+        self.sword_folder_tab = QtWidgets.QWidget(self.sword_tab_widget)
+        self.sword_folder_tab.setObjectName('SwordFolderTab')
+        self.sword_folder_tab_layout = QtWidgets.QGridLayout(self.sword_folder_tab)
+        self.sword_folder_tab_layout.setObjectName('SwordTabFolderLayout')
+        self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab)
+        self.sword_folder_label.setObjectName('SwordSourceLabel')
+        self.sword_folder_tab_layout.addWidget(self.sword_folder_label, 0, 0)
+        self.sword_folder_label.setObjectName('SwordFolderLabel')
+        self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab)
+        self.sword_folder_edit.setObjectName('SwordFolderEdit')
+        self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab)
+        self.sword_browse_button.setIcon(self.open_icon)
+        self.sword_browse_button.setObjectName('SwordBrowseButton')
+        self.sword_folder_tab_layout.addWidget(self.sword_folder_edit, 0, 1)
+        self.sword_folder_tab_layout.addWidget(self.sword_browse_button, 0, 2)
+        self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab)
+        self.sword_bible_label.setObjectName('SwordBibleLabel')
+        self.sword_folder_tab_layout.addWidget(self.sword_bible_label, 1, 0)
+        self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab)
+        self.sword_bible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
+        self.sword_bible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
+        self.sword_bible_combo_box.setObjectName('SwordBibleComboBox')
+        self.sword_folder_tab_layout.addWidget(self.sword_bible_combo_box, 1, 1)
+        self.sword_tab_widget.addTab(self.sword_folder_tab, '')
+        self.sword_zip_tab = QtWidgets.QWidget(self.sword_tab_widget)
+        self.sword_zip_tab.setObjectName('SwordZipTab')
+        self.sword_zip_layout = QtWidgets.QGridLayout(self.sword_zip_tab)
+        self.sword_zip_layout.setObjectName('SwordZipLayout')
+        self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab)
+        self.sword_zipfile_label.setObjectName('SwordZipFileLabel')
+        self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab)
+        self.sword_zipfile_edit.setObjectName('SwordZipFileEdit')
+        self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab)
+        self.sword_zipbrowse_button.setIcon(self.open_icon)
+        self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton')
+        self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab)
+        self.sword_zipbible_label.setObjectName('SwordZipBibleLabel')
+        self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab)
+        self.sword_zipbible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
+        self.sword_zipbible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
+        self.sword_zipbible_combo_box.setObjectName('SwordZipBibleComboBox')
+        self.sword_zip_layout.addWidget(self.sword_zipfile_label, 0, 0)
+        self.sword_zip_layout.addWidget(self.sword_zipfile_edit, 0, 1)
+        self.sword_zip_layout.addWidget(self.sword_zipbrowse_button, 0, 2)
+        self.sword_zip_layout.addWidget(self.sword_zipbible_label, 1, 0)
+        self.sword_zip_layout.addWidget(self.sword_zipbible_combo_box, 1, 1)
+        self.sword_tab_widget.addTab(self.sword_zip_tab, '')
+        self.sword_layout.addWidget(self.sword_tab_widget)
+        self.sword_disabled_label = QtWidgets.QLabel(self.sword_widget)
+        self.sword_disabled_label.setObjectName('SwordDisabledLabel')
+        self.sword_layout.addWidget(self.sword_disabled_label)
+        self.select_stack.addWidget(self.sword_widget)
         self.select_page_layout.addLayout(self.select_stack)
         self.addPage(self.select_page)
         # License Page
@@ -323,6 +401,7 @@
         self.format_combo_box.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm',
                                                                              'Web Download'))
         self.format_combo_box.setItemText(BibleFormat.Zefania, WizardStrings.ZEF)
+        self.format_combo_box.setItemText(BibleFormat.SWORD, WizardStrings.SWORD)
         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:'))
@@ -346,6 +425,22 @@
         self.web_tab_widget.setTabText(
             self.web_tab_widget.indexOf(self.web_proxy_tab), translate('BiblesPlugin.ImportWizardForm',
                                                                        'Proxy Server (Optional)'))
+        self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:'))
+        self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:'))
+        self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:'))
+        self.sword_folder_edit.setPlaceholderText(translate('BiblesPlugin.ImportWizardForm',
+                                                            'Defaults to the standard SWORD data folder'))
+        self.sword_zipbible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:'))
+        self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_folder_tab),
+                                         translate('BiblesPlugin.ImportWizardForm', 'Import from folder'))
+        self.sword_tab_widget.setTabText(self.sword_tab_widget.indexOf(self.sword_zip_tab),
+                                         translate('BiblesPlugin.ImportWizardForm', 'Import from Zip-file'))
+        if PYSWORD_AVAILABLE:
+            self.sword_disabled_label.setText('')
+        else:
+            self.sword_disabled_label.setText(translate('BiblesPlugin.ImportWizardForm',
+                                                        'To import SWORD bibles the pysword python module must be '
+                                                        'installed. Please read the manual for instructions.'))
         self.license_details_page.setTitle(
             translate('BiblesPlugin.ImportWizardForm', 'License Details'))
         self.license_details_page.setSubTitle(translate('BiblesPlugin.ImportWizardForm',
@@ -374,6 +469,9 @@
         if self.currentPage() == self.welcome_page:
             return True
         elif self.currentPage() == self.select_page:
+            self.version_name_edit.clear()
+            self.permissions_edit.clear()
+            self.copyright_edit.clear()
             if self.field('source_format') == BibleFormat.OSIS:
                 if not self.field('osis_location'):
                     critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.OSIS)
@@ -410,6 +508,31 @@
                     return False
                 else:
                     self.version_name_edit.setText(self.web_translation_combo_box.currentText())
+            elif self.field('source_format') == BibleFormat.SWORD:
+                # Test the SWORD tab that is currently active
+                if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab):
+                    if not self.field('sword_folder_path') and self.sword_bible_combo_box.count() == 0:
+                        critical_error_message_box(UiStrings().NFSs,
+                                                   WizardStrings.YouSpecifyFolder % WizardStrings.SWORD)
+                        self.sword_folder_edit.setFocus()
+                        return False
+                    key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex())
+                    if 'description' in self.pysword_folder_modules_json[key]:
+                        self.version_name_edit.setText(self.pysword_folder_modules_json[key]['description'])
+                    if 'distributionlicense' in self.pysword_folder_modules_json[key]:
+                        self.permissions_edit.setText(self.pysword_folder_modules_json[key]['distributionlicense'])
+                    if 'copyright' in self.pysword_folder_modules_json[key]:
+                        self.copyright_edit.setText(self.pysword_folder_modules_json[key]['copyright'])
+                elif self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_zip_tab):
+                    if not self.field('sword_zip_path'):
+                        critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.SWORD)
+                        self.sword_zipfile_edit.setFocus()
+                        return False
+                    key = self.sword_zipbible_combo_box.itemData(self.sword_zipbible_combo_box.currentIndex())
+                    if 'description' in self.pysword_zip_modules_json[key]:
+                        self.version_name_edit.setText(self.pysword_zip_modules_json[key]['description'])
+                    if 'distributionlicense' in self.pysword_zip_modules_json[key]:
+                        self.permissions_edit.setText(self.pysword_zip_modules_json[key]['distributionlicense'])
             return True
         elif self.currentPage() == self.license_details_page:
             license_version = self.field('license_version')
@@ -531,6 +654,40 @@
         self.web_update_button.setEnabled(True)
         self.web_progress_bar.setVisible(False)
 
+    def on_sword_browse_button_clicked(self):
+        """
+        Show the file open dialog for the SWORD folder.
+        """
+        self.get_folder(WizardStrings.OpenTypeFolder % WizardStrings.SWORD, self.sword_folder_edit,
+                        'last directory import')
+        if self.sword_folder_edit.text():
+            try:
+                self.pysword_folder_modules = modules.SwordModules(self.sword_folder_edit.text())
+                self.pysword_folder_modules_json = self.pysword_folder_modules.parse_modules()
+                bible_keys = self.pysword_folder_modules_json.keys()
+                self.sword_bible_combo_box.clear()
+                for key in bible_keys:
+                    self.sword_bible_combo_box.addItem(self.pysword_folder_modules_json[key]['description'], key)
+            except:
+                self.sword_bible_combo_box.clear()
+
+    def on_sword_zipbrowse_button_clicked(self):
+        """
+        Show the file open dialog for a SWORD zip-file.
+        """
+        self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.SWORD, self.sword_zipfile_edit,
+                           'last directory import')
+        if self.sword_zipfile_edit.text():
+            try:
+                self.pysword_zip_modules = modules.SwordModules(self.sword_zipfile_edit.text())
+                self.pysword_zip_modules_json = self.pysword_zip_modules.parse_modules()
+                bible_keys = self.pysword_zip_modules_json.keys()
+                self.sword_zipbible_combo_box.clear()
+                for key in bible_keys:
+                    self.sword_zipbible_combo_box.addItem(self.pysword_zip_modules_json[key]['description'], key)
+            except:
+                self.sword_zipbible_combo_box.clear()
+
     def register_fields(self):
         """
         Register the bible import wizard fields.
@@ -543,6 +700,8 @@
         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('sword_folder_path', self.sword_folder_edit)
+        self.select_page.registerField('sword_zip_path', self.sword_zipfile_edit)
         self.select_page.registerField('proxy_server', self.web_server_edit)
         self.select_page.registerField('proxy_username', self.web_user_edit)
         self.select_page.registerField('proxy_password', self.web_password_edit)
@@ -565,6 +724,8 @@
         self.setField('csv_versefile', '')
         self.setField('opensong_file', '')
         self.setField('zefania_file', '')
+        self.setField('sword_folder_path', '')
+        self.setField('sword_zip_path', '')
         self.setField('web_location', WebDownload.Crosswalk)
         self.setField('web_biblename', self.web_translation_combo_box.currentIndex())
         self.setField('proxy_server', settings.value('proxy address'))
@@ -626,9 +787,21 @@
                 language_id=language_id
             )
         elif bible_type == BibleFormat.Zefania:
-            # Import an Zefania bible.
+            # Import a Zefania bible.
             importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version,
                                                  filename=self.field('zefania_file'))
+        elif bible_type == BibleFormat.SWORD:
+            # Import a SWORD bible.
+            if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab):
+                importer = self.manager.import_bible(BibleFormat.SWORD, name=license_version,
+                                                     sword_path=self.field('sword_folder_path'),
+                                                     sword_key=self.sword_bible_combo_box.itemData(
+                                                         self.sword_bible_combo_box.currentIndex()))
+            else:
+                importer = self.manager.import_bible(BibleFormat.SWORD, name=license_version,
+                                                     sword_path=self.field('sword_zip_path'),
+                                                     sword_key=self.sword_zipbible_combo_box.itemData(
+                                                         self.sword_zipbible_combo_box.currentIndex()))
         if importer.do_import(license_version):
             self.manager.save_meta_data(license_version, license_version, license_copyright, license_permissions)
             self.manager.reload_bibles()

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2016-04-05 17:10:51 +0000
+++ openlp/plugins/bibles/lib/manager.py	2016-04-23 18:44:10 +0000
@@ -31,7 +31,10 @@
 from .opensong import OpenSongBible
 from .osis import OSISBible
 from .zefania import ZefaniaBible
-
+try:
+    from .sword import SwordBible
+except:
+    pass
 
 log = logging.getLogger(__name__)
 
@@ -46,6 +49,7 @@
     OpenSong = 2
     WebDownload = 3
     Zefania = 4
+    SWORD = 5
 
     @staticmethod
     def get_class(bible_format):
@@ -64,6 +68,8 @@
             return HTTPBible
         elif bible_format == BibleFormat.Zefania:
             return ZefaniaBible
+        elif bible_format == BibleFormat.SWORD:
+            return SwordBible
         else:
             return None
 
@@ -78,6 +84,7 @@
             BibleFormat.OpenSong,
             BibleFormat.WebDownload,
             BibleFormat.Zefania,
+            BibleFormat.SWORD
         ]
 
 

=== added file 'openlp/plugins/bibles/lib/sword.py'
--- openlp/plugins/bibles/lib/sword.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/bibles/lib/sword.py	2016-04-23 18:44:10 +0000
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# 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 pysword import modules
+
+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 SwordBible(BibleDB):
+    """
+    SWORD Bible format importer class.
+    """
+    def __init__(self, parent, **kwargs):
+        """
+        Constructor to create and set up an instance of the SwordBible class. This class is used to import Bibles
+        from SWORD bible modules.
+        """
+        log.debug(self.__class__.__name__)
+        BibleDB.__init__(self, parent, **kwargs)
+        self.sword_key = kwargs['sword_key']
+        self.sword_path = kwargs['sword_path']
+        if self.sword_path == '':
+            self.sword_path = None
+
+    def do_import(self, bible_name=None):
+        """
+        Loads a Bible from SWORD module.
+        """
+        log.debug('Starting SWORD import from "%s"' % self.sword_key)
+        success = True
+        try:
+            pysword_modules = modules.SwordModules(self.sword_path)
+            pysword_module_json = pysword_modules.parse_modules()[self.sword_key]
+            bible = pysword_modules.get_bible_from_module(self.sword_key)
+            language = pysword_module_json['lang']
+            language = language[language.find('.') + 1:]
+            language_id = BiblesResourcesDB.get_language(language)['id']
+            self.save_meta('language_id', language_id)
+            books = bible.get_structure().get_books()
+            # Count number of books
+            num_books = 0
+            if 'ot' in books:
+                num_books += len(books['ot'])
+            if 'nt' in books:
+                num_books += len(books['nt'])
+            self.wizard.progress_bar.setMaximum(num_books)
+            # Import the bible
+            for testament in books.keys():
+                for book in books[testament]:
+                    book_ref_id = self.get_book_ref_id_by_name(book.name, num_books, language_id)
+                    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_number in range(1, book.num_chapters + 1):
+                        if self.stop_import_flag:
+                            break
+                        verses = bible.get_iter(book.name, chapter_number)
+                        verse_number = 0
+                        for verse in verses:
+                            verse_number += 1
+                            self.create_verse(db_book.id, chapter_number, verse_number, verse)
+                    self.wizard.increment_progress_bar(
+                        translate('BiblesPlugin.Sword', 'Importing %s...') % db_book.name)
+            self.session.commit()
+            self.application.process_events()
+        except Exception as e:
+            critical_error_message_box(
+                message=translate('BiblesPlugin.SwordImport', 'An unexpected error happened while importing the SWORD '
+                                                              'bible, please report this to the OpenLP developers.\n'
+                                                              '%s' % e))
+            log.exception(str(e))
+            success = False
+        if self.stop_import_flag:
+            return False
+        else:
+            return success

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2015-12-31 22:46:06 +0000
+++ scripts/check_dependencies.py	2016-04-23 18:44:10 +0000
@@ -102,6 +102,7 @@
     ('nose', '(testing framework)', True),
     ('mock', '(testing module)', sys.version_info[1] < 3),
     ('jenkins', '(access jenkins api - package name: jenkins-webapi)', True),
+    ('pysword', '(import SWORD bibles)', True),
 ]
 
 w = sys.stdout.write

=== added file 'tests/functional/openlp_plugins/bibles/test_swordimport.py'
--- tests/functional/openlp_plugins/bibles/test_swordimport.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/bibles/test_swordimport.py	2016-04-23 18:44:10 +0000
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# 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 SWORD Bible importer.
+"""
+
+import os
+import json
+from unittest import TestCase, SkipTest
+
+from tests.functional import MagicMock, patch
+try:
+    from openlp.plugins.bibles.lib.sword import SwordBible
+except ImportError:
+    raise SkipTest('PySword is not installed, skipping SWORD test.')
+from openlp.plugins.bibles.lib.db import BibleDB
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                         '..', '..', '..', 'resources', 'bibles'))
+
+
+class TestSwordImport(TestCase):
+    """
+    Test the functions in the :mod:`swordimport` 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 Sword file importer
+        """
+        # GIVEN: A mocked out "manager"
+        mocked_manager = MagicMock()
+
+        # WHEN: An importer object is created
+        importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='')
+
+        # THEN: The importer should be an instance of BibleDB
+        self.assertIsInstance(importer, BibleDB)
+
+    @patch('openlp.plugins.bibles.lib.sword.SwordBible.application')
+    @patch('openlp.plugins.bibles.lib.sword.modules')
+    @patch('openlp.plugins.bibles.lib.db.BiblesResourcesDB')
+    def simple_import_test(self, mocked_bible_res_db, mocked_pysword_modules, mocked_application):
+        """
+        Test that a simple SWORD import works
+        """
+        # 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.
+        #       Also mocked pysword structures
+        mocked_manager = MagicMock()
+        mocked_import_wizard = MagicMock()
+        importer = SwordBible(mocked_manager, path='.', name='.', filename='', sword_key='', sword_path='')
+        result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb')
+        test_data = json.loads(result_file.read().decode())
+        importer.wizard = mocked_import_wizard
+        importer.get_book_ref_id_by_name = MagicMock()
+        importer.create_verse = MagicMock()
+        importer.create_book = MagicMock()
+        importer.session = MagicMock()
+        mocked_bible_res_db.get_language.return_value = 'Danish'
+        mocked_bible = MagicMock()
+        mocked_genesis = MagicMock()
+        mocked_genesis.name = 'Genesis'
+        mocked_genesis.num_chapters = 1
+        books = {'ot': [mocked_genesis]}
+        mocked_structure = MagicMock()
+        mocked_structure.get_books.return_value = books
+        mocked_bible.get_structure.return_value = mocked_structure
+        mocked_bible.get_iter.return_value = [verse[1] for verse in test_data['verses']]
+        mocked_module = MagicMock()
+        mocked_module.get_bible_from_module.return_value = mocked_bible
+        mocked_pysword_modules.SwordModules.return_value = mocked_module
+
+        # WHEN: Importing 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 test_data['verses']:
+            importer.create_verse.assert_any_call(importer.create_book().id, 1, int(verse_tag), verse_text)

=== modified file 'tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py'
--- tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py	2015-12-31 22:46:06 +0000
+++ tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py	2016-04-23 18:44:10 +0000
@@ -27,7 +27,7 @@
 from PyQt5 import QtWidgets
 
 from openlp.core.common import Registry
-from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm, WebDownload
+import openlp.plugins.bibles.forms.bibleimportform as bibleimportform
 
 from tests.helpers.testmixin import TestMixin
 from tests.functional import MagicMock, patch
@@ -46,7 +46,8 @@
         self.setup_application()
         self.main_window = QtWidgets.QMainWindow()
         Registry().register('main_window', self.main_window)
-        self.form = BibleImportForm(self.main_window, MagicMock(), MagicMock())
+        bibleimportform.PYSWORD_AVAILABLE = False
+        self.form = bibleimportform.BibleImportForm(self.main_window, MagicMock(), MagicMock())
 
     def tearDown(self):
         """
@@ -76,3 +77,16 @@
 
         # THEN: The webbible list should still be empty
         self.assertEqual(self.form.web_bible_list, {}, 'The webbible list should be empty')
+
+    def custom_init_test(self):
+        """
+        Test that custom_init works as expected if pysword is unavailable
+        """
+        # GIVEN: A mocked sword_tab_widget
+        self.form.sword_tab_widget = MagicMock()
+
+        # WHEN: Running custom_init
+        self.form.custom_init()
+
+        # THEN: sword_tab_widget.setDisabled(True) should have been called
+        self.form.sword_tab_widget.setDisabled.assert_called_with(True)


Follow ups