From 1ebc6737d77be5349ae2ba2f2c6e573bf5b80b4f Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 18 Jan 2020 21:45:47 +0000 Subject: [PATCH] Made the bibel and song import more robust. Tested the importers by loading a text file, a png file and an xml file. --- appveyor.yml | 2 +- openlp/core/common/settings.py | 2 +- openlp/core/ui/style.py | 5 +- .../plugins/bibles/forms/bibleimportform.py | 7 + .../plugins/bibles/lib/importers/csvbible.py | 5 +- .../bibles/lib/importers/wordproject.py | 31 +++- .../plugins/bibles/lib/importers/zefania.py | 7 +- openlp/plugins/songs/forms/songimportform.py | 5 +- .../plugins/songs/lib/importers/cclifile.py | 15 +- .../plugins/songs/lib/importers/chordpro.py | 10 +- .../plugins/songs/lib/importers/dreambeam.py | 17 +- .../plugins/songs/lib/importers/easyslides.py | 31 +++- .../songs/lib/importers/easyworship.py | 17 +- .../songs/lib/importers/foilpresenter.py | 4 + openlp/plugins/songs/lib/importers/openlp.py | 19 +- openlp/plugins/songs/lib/importers/opspro.py | 2 +- .../songs/lib/importers/powerpraise.py | 95 ++++++---- .../lib/importers/presentationmanager.py | 19 +- .../songs/lib/importers/propresenter.py | 25 ++- .../plugins/songs/lib/importers/songbeamer.py | 9 +- openlp/plugins/songs/lib/importers/songpro.py | 12 +- .../songs/lib/importers/songshowplus.py | 162 +++++++++--------- .../songs/lib/importers/worshipassistant.py | 17 +- .../songs/lib/importers/worshipcenterpro.py | 2 +- .../plugins/songs/lib/importers/zionworx.py | 66 +++---- tests/functional/openlp_core/ui/test_style.py | 6 +- .../bibles/test_wordprojectimport.py | 10 +- 27 files changed, 393 insertions(+), 209 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 381fb8af4..3e8108688 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,7 +29,7 @@ test_script: after_test: # This is where we create a package using PyInstaller # Install PyInstaller - - "%PYTHON%\\python.exe -m pip install pyinstaller" + - "%PYTHON%\\python.exe -m pip install pyinstaller==3.5" # Disabled portable installers - can't figure out how to make them silent # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe # - PortableApps.comInstaller_3.4.4.paf.exe /S diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index d58f028b6..1d8d24349 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -306,7 +306,7 @@ class Settings(QtCore.QSettings): 'songs/db hostname': '', 'songs/db database': '', 'songs/last used search type': SongSearch.Entire, - 'songs/last import type': None, + 'songs/last import type': 0, 'songs/update service on edit': False, 'songs/add song from service': True, 'songs/add songbook slide': False, diff --git a/openlp/core/ui/style.py b/openlp/core/ui/style.py index 947c9e665..a5bc565a5 100644 --- a/openlp/core/ui/style.py +++ b/openlp/core/ui/style.py @@ -21,10 +21,9 @@ """ The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme """ -from PyQt5 import QtGui +from PyQt5 import QtGui, QtWidgets from openlp.core.common import is_win -from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -88,7 +87,7 @@ def get_application_stylesheet(): stylesheet = qdarkstyle.load_stylesheet_pyqt5() else: if not Settings().value('advanced/alternate rows'): - base_color = Registry().get('application').palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) + base_color = QtWidgets.QApplication.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base) alternate_rows_repair_stylesheet = \ 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' stylesheet += alternate_rows_repair_stylesheet diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 451e61545..abd3a77b0 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -40,6 +40,7 @@ from openlp.core.common.settings import Settings from openlp.core.lib.db import delete_database from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.ui import critical_error_message_box +from openlp.core.widgets.enums import PathEditType from openlp.core.widgets.edits import PathEdit from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.bibles.lib.db import clean_filename @@ -276,6 +277,7 @@ class BibleImportForm(OpenLPWizard): self.sword_folder_label.setObjectName('SwordFolderLabel') self.sword_folder_path_edit = PathEdit( self.sword_folder_tab, + path_type=PathEditType.Directories, default_path=Settings().value('bibles/last directory import'), dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD), show_revert=False, @@ -502,6 +504,11 @@ class BibleImportForm(OpenLPWizard): self.sword_folder_path_edit.setFocus() return False key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex()) + if not key: + critical_error_message_box(UiStrings().NFSs, + WizardStrings.YouSpecifyFolder % WizardStrings.SWORD) + self.sword_folder_path_edit.setFocus() + return False 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]: diff --git a/openlp/plugins/bibles/lib/importers/csvbible.py b/openlp/plugins/bibles/lib/importers/csvbible.py index d248e1a2f..38a926700 100644 --- a/openlp/plugins/bibles/lib/importers/csvbible.py +++ b/openlp/plugins/bibles/lib/importers/csvbible.py @@ -49,6 +49,7 @@ There are two acceptable formats of the verses file. They are: All CSV files are expected to use a comma (',') as the delimiter and double quotes ('"') as the quote symbol. """ import csv +import logging from collections import namedtuple from openlp.core.common import get_file_encoding @@ -56,6 +57,7 @@ from openlp.core.common.i18n import translate from openlp.core.lib.exceptions import ValidationError from openlp.plugins.bibles.lib.bibleimport import BibleImport +log = logging.getLogger(__name__) Book = namedtuple('Book', 'id, testament_id, name, abbreviation') Verse = namedtuple('Verse', 'book_id_name, chapter_number, number, text') @@ -105,7 +107,8 @@ class CSVBible(BibleImport): with file_path.open('r', encoding=encoding, newline='') as csv_file: csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"') return [results_tuple(*line) for line in csv_reader] - except (OSError, csv.Error): + except (OSError, csv.Error, TypeError, UnicodeDecodeError): + log.exception('Parsing {file} failed.'.format(file=file_path)) raise ValidationError(msg='Parsing "{file}" failed'.format(file=file_path)) def process_books(self, books): diff --git a/openlp/plugins/bibles/lib/importers/wordproject.py b/openlp/plugins/bibles/lib/importers/wordproject.py index 72d7b2458..2e9a96a0a 100644 --- a/openlp/plugins/bibles/lib/importers/wordproject.py +++ b/openlp/plugins/bibles/lib/importers/wordproject.py @@ -22,10 +22,12 @@ import logging import re from pathlib import Path from tempfile import TemporaryDirectory -from zipfile import ZipFile +from zipfile import ZipFile, BadZipFile from bs4 import BeautifulSoup, NavigableString, Tag +from openlp.core.common.i18n import translate +from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.bibleimport import BibleImport @@ -50,18 +52,30 @@ class WordProjectBible(BibleImport): Unzip the file to a temporary directory """ self.tmp = TemporaryDirectory() - with ZipFile(self.file_path) as zip_file: - zip_file.extractall(self.tmp.name) + try: + with ZipFile(self.file_path) as zip_file: + zip_file.extractall(self.tmp.name) + except BadZipFile: + self.log_exception('Extracting {file} failed.'.format(file=self.file_path)) + critical_error_message_box(message=translate('BiblesPlugin.WordProjectBible', + 'Incorrect Bible file type, not a Zip file.')) + return False self.base_path = Path(self.tmp.name, self.file_path.stem) + return True def process_books(self): """ Extract and create the bible books from the parsed html :param bible_data: parsed xml - :return: None + :return: True if books was parsed, otherwise False """ - page = (self.base_path / 'index.htm').read_text(encoding='utf-8', errors='ignore') + idx_file = (self.base_path / 'index.htm') + if not idx_file.exists(): + critical_error_message_box(message=translate('BiblesPlugin.WordProjectBible', + 'Incorrect Bible file type, files are missing.')) + return False + page = idx_file.read_text(encoding='utf-8', errors='ignore') soup = BeautifulSoup(page, 'lxml') bible_books = soup.find('div', 'textOptions').find_all('li') book_count = len(bible_books) @@ -81,6 +95,7 @@ class WordProjectBible(BibleImport): db_book = self.find_and_create_book(book_name, book_count, self.language_id, book_id) self.process_chapters(db_book, book_id, book_link) self.session.commit() + return True def process_chapters(self, db_book, book_id, book_link): """ @@ -154,11 +169,11 @@ class WordProjectBible(BibleImport): Loads a Bible from file. """ self.log_debug('Starting WordProject import from "{name}"'.format(name=self.file_path)) - self._unzip_file() + if not self._unzip_file(): + return False self.language_id = self.get_language_id(None, bible_name=str(self.file_path)) result = False if self.language_id: - self.process_books() - result = True + result = self.process_books() self._cleanup() return result diff --git a/openlp/plugins/bibles/lib/importers/zefania.py b/openlp/plugins/bibles/lib/importers/zefania.py index fd385468b..2db0646e4 100644 --- a/openlp/plugins/bibles/lib/importers/zefania.py +++ b/openlp/plugins/bibles/lib/importers/zefania.py @@ -54,7 +54,12 @@ class ZefaniaBible(BibleImport): if not language_id: return False no_of_books = int(xmlbible.xpath('count(//BIBLEBOOK)')) - self.wizard.progress_bar.setMaximum(int(xmlbible.xpath('count(//CHAPTER)'))) + no_of_chap = int(xmlbible.xpath('count(//CHAPTER)')) + if not no_of_books or not no_of_chap: + critical_error_message_box(message=translate('BiblesPlugin.ZefaniaImport', + 'Incorrect Bible file type. Expected data is missing.')) + return False + self.wizard.progress_bar.setMaximum(no_of_chap) for BIBLEBOOK in xmlbible: if self.stop_import_flag: break diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 0448a858b..0239dc6a6 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -396,7 +396,10 @@ class SongImportForm(OpenLPWizard, RegistryProperties): dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name) path_edit = PathEdit( parent=import_widget, path_type=path_type, dialog_caption=dialog_caption, show_revert=False) - path_edit.filters = path_edit.filters + filters + if path_edit.filters: + path_edit.filters = filters + ';;' + path_edit.filters + else: + path_edit.filters = filters path_edit.path = Settings().value(self.plugin.settings_section + '/last directory import') file_path_layout.addWidget(path_edit) import_layout.addLayout(file_path_layout) diff --git a/openlp/plugins/songs/lib/importers/cclifile.py b/openlp/plugins/songs/lib/importers/cclifile.py index f36c58170..d0a6cac6d 100644 --- a/openlp/plugins/songs/lib/importers/cclifile.py +++ b/openlp/plugins/songs/lib/importers/cclifile.py @@ -66,11 +66,16 @@ class CCLIFileImport(SongImport): except UnicodeDecodeError: details = chardet.detect(detect_content) in_file = codecs.open(file_path, 'r', details['encoding']) - if not in_file.read(1) == '\ufeff': - # not UTF or no BOM was found - in_file.seek(0) - lines = in_file.readlines() - in_file.close() + try: + if not in_file.read(1) == '\ufeff': + # not UTF or no BOM was found + in_file.seek(0) + lines = in_file.readlines() + in_file.close() + except UnicodeDecodeError: + self.log_error(file_path, translate('SongsPlugin.CCLIFileImport', + 'The file contains unreadable characters.')) + continue ext = file_path.suffix.lower() if ext == '.usr' or ext == '.bin': log.info('SongSelect USR format file found: {name}'.format(name=file_path)) diff --git a/openlp/plugins/songs/lib/importers/chordpro.py b/openlp/plugins/songs/lib/importers/chordpro.py index 2fc561f5e..3fb6932c6 100644 --- a/openlp/plugins/songs/lib/importers/chordpro.py +++ b/openlp/plugins/songs/lib/importers/chordpro.py @@ -25,6 +25,7 @@ ChordPro files into the current database. import logging import re +from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.db import AuthorType @@ -60,7 +61,12 @@ class ChordProImport(SongImport): """ self.set_defaults() # Loop over the lines of the file - file_content = song_file.read() + try: + file_content = song_file.read() + except UnicodeDecodeError: + self.log_error(song_file.name, translate('SongsPlugin.CCLIFileImport', + 'The file contains unreadable characters.')) + return current_verse = '' current_verse_type = 'v' skip_block = False @@ -181,7 +187,7 @@ class ChordProImport(SongImport): current_verse = re.sub(r'\[.*?\]', '', current_verse) self.add_verse(current_verse.rstrip(), current_verse_type) # if no title was in directives, get it from the first line - if not self.title: + if not self.title and self.verses: (verse_def, verse_text, lang) = self.verses[0] # strip any chords from the title self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0]) diff --git a/openlp/plugins/songs/lib/importers/dreambeam.py b/openlp/plugins/songs/lib/importers/dreambeam.py index 80f83f579..091873151 100644 --- a/openlp/plugins/songs/lib/importers/dreambeam.py +++ b/openlp/plugins/songs/lib/importers/dreambeam.py @@ -87,21 +87,30 @@ class DreamBeamImport(SongImport): return self.set_defaults() author_copyright = '' - parser = etree.XMLParser(remove_blank_text=True) + parser = etree.XMLParser(remove_blank_text=True, recover=True) try: with file_path.open('r') as xml_file: parsed_file = etree.parse(xml_file, parser) except etree.XMLSyntaxError: - log.exception('XML syntax error in file_path {name}'.format(name=file_path)) + log.exception('XML syntax error in file {name}'.format(name=file_path)) self.log_error(file_path, SongStrings.XMLSyntaxError) continue - xml = etree.tostring(parsed_file).decode() + except UnicodeDecodeError: + log.exception('Unreadable characters in {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + file_str = etree.tostring(parsed_file) + if not file_str: + log.exception('Could not find XML in file {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + xml = file_str.decode() song_xml = objectify.fromstring(xml) if song_xml.tag != 'DreamSong': self.log_error( file_path, translate('SongsPlugin.DreamBeamImport', - 'Invalid DreamBeam song file_path. Missing DreamSong tag.')) + 'Invalid DreamBeam song file. Missing DreamSong tag.')) continue if hasattr(song_xml, 'Version'): self.version = float(song_xml.Version.text) diff --git a/openlp/plugins/songs/lib/importers/easyslides.py b/openlp/plugins/songs/lib/importers/easyslides.py index fe06b72a9..1defd743b 100644 --- a/openlp/plugins/songs/lib/importers/easyslides.py +++ b/openlp/plugins/songs/lib/importers/easyslides.py @@ -25,9 +25,10 @@ import re from lxml import etree, objectify from openlp.core.common import normalize_str +from openlp.core.common.i18n import translate from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport - +from openlp.plugins.songs.lib.ui import SongStrings log = logging.getLogger(__name__) @@ -48,10 +49,32 @@ class EasySlidesImport(SongImport): def do_import(self): log.info('Importing EasySlides XML file {source}'.format(source=self.import_source)) parser = etree.XMLParser(remove_blank_text=True, recover=True) - parsed_file = etree.parse(str(self.import_source), parser) - xml = etree.tostring(parsed_file).decode() + try: + with self.import_source.open('r') as xml_file: + parsed_file = etree.parse(str(self.import_source), parser) + except etree.XMLSyntaxError: + log.exception('XML syntax error in file {name}'.format(name=xml_file)) + self.log_error(self.import_source, SongStrings.XMLSyntaxError) + return + except UnicodeDecodeError: + log.exception('Unreadable characters in {name}'.format(name=xml_file)) + self.log_error(self.import_source, SongStrings.XMLSyntaxError) + return + file_str = etree.tostring(parsed_file) + if not file_str: + log.exception('Could not find XML in file {name}'.format(name=xml_file)) + self.log_error(self.import_source, SongStrings.XMLSyntaxError) + return + xml = file_str.decode() song_xml = objectify.fromstring(xml) - self.import_wizard.progress_bar.setMaximum(len(song_xml.Item)) + try: + item_count = len(song_xml.Item) + except AttributeError: + log.exception('No root attribute "Item"') + self.log_error(self.import_source, translate('SongsPlugin.EasySlidesImport', + 'Invalid EasySlides song file. Missing Item tag.')) + return + self.import_wizard.progress_bar.setMaximum(item_count) for song in song_xml.Item: if self.stop_import_flag: return diff --git a/openlp/plugins/songs/lib/importers/easyworship.py b/openlp/plugins/songs/lib/importers/easyworship.py index f84d172f3..c4fce1fc6 100644 --- a/openlp/plugins/songs/lib/importers/easyworship.py +++ b/openlp/plugins/songs/lib/importers/easyworship.py @@ -78,12 +78,17 @@ class EasyWorshipSongImport(SongImport): """ self.import_source = Path(self.import_source) ext = self.import_source.suffix.lower() - if ext == '.ews': - self.import_ews() - elif ext == '.db': - self.import_db() - else: - self.import_sqlite_db() + try: + if ext == '.ews': + self.import_ews() + elif ext == '.db': + self.import_db() + else: + self.import_sqlite_db() + except Exception: + log.exception('Unexpected data in file {name}'.format(name=self.import_source)) + self.log_error(self.import_source, + '{name} contains unexpected data and can not be imported'.format(name=self.import_source)) def import_ews(self): """ diff --git a/openlp/plugins/songs/lib/importers/foilpresenter.py b/openlp/plugins/songs/lib/importers/foilpresenter.py index 27e95153d..65ded09ca 100644 --- a/openlp/plugins/songs/lib/importers/foilpresenter.py +++ b/openlp/plugins/songs/lib/importers/foilpresenter.py @@ -127,6 +127,10 @@ class FoilPresenterImport(SongImport): except etree.XMLSyntaxError: self.log_error(file_path, SongStrings.XMLSyntaxError) log.exception('XML syntax error in file {path}'.format(path=file_path)) + except AttributeError: + self.log_error(file_path, translate('SongsPlugin.FoilPresenterSongImport', + 'Invalid Foilpresenter song file. Missing expected tags')) + log.exception('Missing content in file {path}'.format(path=file_path)) class FoilPresenter(object): diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index fdf7ccb42..7656dbc12 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -130,13 +130,18 @@ class OpenLPSongImport(SongImport): else: has_authors_songs = False # Load up the tabls and map them out - source_authors_table = source_meta.tables['authors'] - source_song_books_table = source_meta.tables['song_books'] - source_songs_table = source_meta.tables['songs'] - source_topics_table = source_meta.tables['topics'] - source_authors_songs_table = source_meta.tables['authors_songs'] - source_songs_topics_table = source_meta.tables['songs_topics'] - source_media_files_songs_table = None + try: + source_authors_table = source_meta.tables['authors'] + source_song_books_table = source_meta.tables['song_books'] + source_songs_table = source_meta.tables['songs'] + source_topics_table = source_meta.tables['topics'] + source_authors_songs_table = source_meta.tables['authors_songs'] + source_songs_topics_table = source_meta.tables['songs_topics'] + source_media_files_songs_table = None + except KeyError: + self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport', + 'Not a valid OpenLP 2 song database.')) + return # Set up media_files relations if has_media_files: source_media_files_table = source_meta.tables['media_files'] diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index a205b70a8..f6cda6af6 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -58,7 +58,7 @@ class OPSProImport(SongImport): try: conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};' 'PWD={password}'.format(source=self.import_source, password=password)) - except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: + except Exception as e: log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source, error=str(e))) # Unfortunately no specific exception type diff --git a/openlp/plugins/songs/lib/importers/powerpraise.py b/openlp/plugins/songs/lib/importers/powerpraise.py index 19185da75..88395c26e 100644 --- a/openlp/plugins/songs/lib/importers/powerpraise.py +++ b/openlp/plugins/songs/lib/importers/powerpraise.py @@ -22,10 +22,16 @@ The :mod:`powerpraiseimport` module provides the functionality for importing Powerpraise song files into the current database. """ -from lxml import objectify +import logging +from lxml import objectify, etree + +from openlp.core.common.i18n import translate from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib.importers.songimport import SongImport +from openlp.plugins.songs.lib.ui import SongStrings + +log = logging.getLogger(__name__) class PowerPraiseImport(SongImport): @@ -40,43 +46,56 @@ class PowerPraiseImport(SongImport): return self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name)) with file_path.open('rb') as xml_file: - root = objectify.parse(xml_file).getroot() - self.process_song(root) + try: + root = objectify.parse(xml_file).getroot() + except etree.XMLSyntaxError: + log.exception('XML syntax error in file {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + except UnicodeDecodeError: + log.exception('Unreadable characters in {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + self.process_song(root, file_path) - def process_song(self, root): + def process_song(self, root, file_path): self.set_defaults() - self.title = str(root.general.title) - verse_order_list = [] - verse_count = {} - for item in root.order.item: - verse_order_list.append(str(item)) - for part in root.songtext.part: - original_verse_def = part.get('caption') - # There are some predefined verse defitions in PowerPraise, try to parse these - if original_verse_def.startswith("Strophe") or original_verse_def.startswith("Teil"): - verse_def = 'v' - elif original_verse_def.startswith("Refrain"): - verse_def = 'c' - elif original_verse_def.startswith("Bridge"): - verse_def = 'b' - elif original_verse_def.startswith("Schluss"): - verse_def = 'e' - else: - verse_def = 'o' - verse_count[verse_def] = verse_count.get(verse_def, 0) + 1 - verse_def = '{verse}{count:d}'.format(verse=verse_def, count=verse_count[verse_def]) - verse_text = [] - for slide in part.slide: - if not hasattr(slide, 'line'): - continue # No content - for line in slide.line: - verse_text.append(str(line)) - self.add_verse('\n'.join(verse_text), verse_def) - # Update verse name in verse order list - for i in range(len(verse_order_list)): - if verse_order_list[i].lower() == original_verse_def.lower(): - verse_order_list[i] = verse_def + try: + self.title = str(root.general.title) + verse_order_list = [] + verse_count = {} + for item in root.order.item: + verse_order_list.append(str(item)) + for part in root.songtext.part: + original_verse_def = part.get('caption') + # There are some predefined verse defitions in PowerPraise, try to parse these + if original_verse_def.startswith("Strophe") or original_verse_def.startswith("Teil"): + verse_def = 'v' + elif original_verse_def.startswith("Refrain"): + verse_def = 'c' + elif original_verse_def.startswith("Bridge"): + verse_def = 'b' + elif original_verse_def.startswith("Schluss"): + verse_def = 'e' + else: + verse_def = 'o' + verse_count[verse_def] = verse_count.get(verse_def, 0) + 1 + verse_def = '{verse}{count:d}'.format(verse=verse_def, count=verse_count[verse_def]) + verse_text = [] + for slide in part.slide: + if not hasattr(slide, 'line'): + continue # No content + for line in slide.line: + verse_text.append(str(line)) + self.add_verse('\n'.join(verse_text), verse_def) + # Update verse name in verse order list + for i in range(len(verse_order_list)): + if verse_order_list[i].lower() == original_verse_def.lower(): + verse_order_list[i] = verse_def - self.verse_order_list = verse_order_list - if not self.finish(): - self.log_error(self.import_source) + self.verse_order_list = verse_order_list + if not self.finish(): + self.log_error(file_path) + except AttributeError: + self.log_error(file_path, translate('SongsPlugin.PowerPraiseImport', + 'Invalid PowerPraise song file. Missing needed tag.')) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 71354cb89..1a6b465ae 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -22,6 +22,7 @@ The :mod:`presentationmanager` module provides the functionality for importing Presentationmanager song files into the current database. """ +import logging import re from lxml import etree, objectify @@ -31,6 +32,8 @@ from openlp.core.common.i18n import translate from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib.importers.songimport import SongImport +log = logging.getLogger(__name__) + class PresentationManagerImport(SongImport): """ @@ -54,12 +57,24 @@ class PresentationManagerImport(SongImport): try: tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) except ValueError: + log.exception('XML syntax error in file {name}'.format(name=file_path)) self.log_error(file_path, translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - root = objectify.fromstring(etree.tostring(tree)) - self.process_song(root, file_path) + file_str = etree.tostring(tree) + if not file_str: + log.exception('Could not find XML in file {name}'.format(name=file_path)) + self.log_error(file_path, translate('SongsPlugin.PresentationManagerImport', + 'File is not in XML-format, which is the only format supported.')) + continue + root = objectify.fromstring(file_str) + try: + self.process_song(root, file_path) + except AttributeError: + log.exception('XML syntax error in file {name}'.format(name=file_path)) + self.log_error(file_path, translate('SongsPlugin.PresentationManagerImport', + 'File is not a valid PresentationManager XMl file.')) def _get_attr(self, elem, name): """ diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index fec7d080e..b77bf3d1e 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -25,11 +25,13 @@ ProPresenter song files into the current installation database. import base64 import logging -from lxml import objectify +from lxml import objectify, etree +from openlp.core.common.i18n import translate from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import strip_rtf from openlp.plugins.songs.lib.importers.songimport import SongImport +from openlp.plugins.songs.lib.ui import SongStrings log = logging.getLogger(__name__) @@ -48,8 +50,23 @@ class ProPresenterImport(SongImport): self.import_wizard.increment_progress_bar( WizardStrings.ImportingType.format(source=file_path.name)) with file_path.open('rb') as xml_file: - root = objectify.parse(xml_file).getroot() - self.process_song(root, file_path) + try: + root = objectify.parse(xml_file).getroot() + except etree.XMLSyntaxError: + log.exception('XML syntax error in file {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + except UnicodeDecodeError: + log.exception('Unreadable characters in {name}'.format(name=file_path)) + self.log_error(file_path, SongStrings.XMLSyntaxError) + continue + try: + self.process_song(root, file_path) + except AttributeError: + log.exception('XML syntax error in file {name}'.format(name=file_path)) + self.log_error(file_path, translate('SongsPlugin.ProPresenterImport', + 'File is not a valid ProPresenter XMl file.')) + continue def process_song(self, root, file_path): """ @@ -62,7 +79,7 @@ class ProPresenterImport(SongImport): # Extract ProPresenter versionNumber try: self.version = int(root.get('versionNumber')) - except ValueError: + except (ValueError, TypeError): log.debug('ProPresenter versionNumber invalid or missing') return diff --git a/openlp/plugins/songs/lib/importers/songbeamer.py b/openlp/plugins/songs/lib/importers/songbeamer.py index 53195c729..9cca17bbf 100644 --- a/openlp/plugins/songs/lib/importers/songbeamer.py +++ b/openlp/plugins/songs/lib/importers/songbeamer.py @@ -29,6 +29,7 @@ import re from pathlib import Path from openlp.core.common import get_file_encoding, is_macosx, is_win +from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport @@ -130,7 +131,13 @@ class SongBeamerImport(SongImport): if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'): self.input_file_encoding = 'cp1252' with file_path.open(encoding=self.input_file_encoding) as song_file: - song_data = song_file.readlines() + try: + song_data = song_file.readlines() + except UnicodeDecodeError: + log.exception('Unreadable characters in {name}'.format(name=file_path)) + self.log_error(file_path, translate('SongsPlugin.SongBeamerImport', + 'File is not a valid SongBeamer file.')) + continue else: continue self.title = file_path.stem diff --git a/openlp/plugins/songs/lib/importers/songpro.py b/openlp/plugins/songs/lib/importers/songpro.py index 4795f38dc..9cd22f36f 100644 --- a/openlp/plugins/songs/lib/importers/songpro.py +++ b/openlp/plugins/songs/lib/importers/songpro.py @@ -22,12 +22,16 @@ The :mod:`songpro` module provides the functionality for importing SongPro songs into the OpenLP database. """ +import logging import re from pathlib import Path +from openlp.core.common.i18n import translate from openlp.plugins.songs.lib import strip_rtf from openlp.plugins.songs.lib.importers.songimport import SongImport +log = logging.getLogger(__name__) + class SongProImport(SongImport): """ @@ -82,7 +86,13 @@ class SongProImport(SongImport): break file_text = file_line.rstrip() if file_text and file_text[0] == '#': - self.process_section(tag, text.rstrip()) + try: + self.process_section(tag, text.rstrip()) + except ValueError: + log.exception('Missing data in {name}'.format(name=self.import_source)) + self.log_error(self.import_source, translate('SongsPlugin.SongProImport', + 'File is not a valid SongPro file.')) + return tag = file_text[1:] text = '' else: diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 291964733..38984ceda 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -26,6 +26,7 @@ import logging import re import struct +from openlp.core.common.i18n import translate from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib.importers.songimport import SongImport @@ -100,84 +101,91 @@ class SongShowPlusImport(SongImport): self.other_list = {} self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name), 0) with file_path.open('rb') as song_file: - while True: - block_key, = struct.unpack("I", song_file.read(4)) - log.debug('block_key: %d' % block_key) - # The file ends with 4 NULL's - if block_key == 0: - break - next_block_starts, = struct.unpack("I", song_file.read(4)) - next_block_starts += song_file.tell() - if block_key in (VERSE, CHORUS, BRIDGE): - null, verse_no, = struct.unpack("BB", song_file.read(2)) - elif block_key == CUSTOM_VERSE: - null, verse_name_length, = struct.unpack("BB", song_file.read(2)) - verse_name = self.decode(song_file.read(verse_name_length)) - length_descriptor_size, = struct.unpack("B", song_file.read(1)) - log.debug('length_descriptor_size: %d' % length_descriptor_size) - # In the case of song_numbers the number is in the data from the - # current position to the next block starts - if block_key == SONG_NUMBER: - sn_bytes = song_file.read(length_descriptor_size - 1) - self.song_number = int.from_bytes(sn_bytes, byteorder='little') - continue - # Detect if/how long the length descriptor is - if length_descriptor_size == 12 or length_descriptor_size == 20: - length_descriptor, = struct.unpack("I", song_file.read(4)) - elif length_descriptor_size == 2: - length_descriptor = 1 - elif length_descriptor_size == 9: - length_descriptor = 0 - else: - length_descriptor, = struct.unpack("B", song_file.read(1)) - log.debug('length_descriptor: %d' % length_descriptor) - data = song_file.read(length_descriptor) - log.debug(data) - if block_key == TITLE: - self.title = self.decode(data) - elif block_key == AUTHOR: - authors = self.decode(data).split(" / ") - for author in authors: - if author.find(",") != -1: - author_parts = author.split(", ") - author = author_parts[1] + " " + author_parts[0] - self.parse_author(author) - elif block_key == COPYRIGHT: - self.add_copyright(self.decode(data)) - elif block_key == CCLI_NO: - # Try to get the CCLI number even if the field contains additional text - match = re.search(r'\d+', self.decode(data)) - if match: - self.ccli_number = int(match.group()) + try: + while True: + block_key, = struct.unpack("I", song_file.read(4)) + log.debug('block_key: %d' % block_key) + # The file ends with 4 NULL's + if block_key == 0: + break + next_block_starts, = struct.unpack("I", song_file.read(4)) + next_block_starts += song_file.tell() + if block_key in (VERSE, CHORUS, BRIDGE): + null, verse_no, = struct.unpack("BB", song_file.read(2)) + elif block_key == CUSTOM_VERSE: + null, verse_name_length, = struct.unpack("BB", song_file.read(2)) + verse_name = self.decode(song_file.read(verse_name_length)) + length_descriptor_size, = struct.unpack("B", song_file.read(1)) + log.debug('length_descriptor_size: %d' % length_descriptor_size) + # In the case of song_numbers the number is in the data from the + # current position to the next block starts + if block_key == SONG_NUMBER: + sn_bytes = song_file.read(length_descriptor_size - 1) + self.song_number = int.from_bytes(sn_bytes, byteorder='little') + continue + # Detect if/how long the length descriptor is + if length_descriptor_size == 12 or length_descriptor_size == 20: + length_descriptor, = struct.unpack("I", song_file.read(4)) + elif length_descriptor_size == 2: + length_descriptor = 1 + elif length_descriptor_size == 9: + length_descriptor = 0 else: - log.warning("Can't parse CCLI Number from string: {text}".format(text=self.decode(data))) - elif block_key == VERSE: - self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Verse], - number=verse_no)) - elif block_key == CHORUS: - self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Chorus], - number=verse_no)) - elif block_key == BRIDGE: - self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Bridge], - number=verse_no)) - elif block_key == TOPIC: - self.topics.append(self.decode(data)) - elif block_key == COMMENTS: - self.comments = self.decode(data) - elif block_key == VERSE_ORDER: - verse_tag = self.to_openlp_verse_tag(self.decode(data), True) - if verse_tag: - if not isinstance(verse_tag, str): - verse_tag = self.decode(verse_tag) - self.ssp_verse_order_list.append(verse_tag) - elif block_key == SONG_BOOK: - self.song_book_name = self.decode(data) - elif block_key == CUSTOM_VERSE: - verse_tag = self.to_openlp_verse_tag(verse_name) - self.add_verse(self.decode(data), verse_tag) - else: - log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data)) - song_file.seek(next_block_starts) + length_descriptor, = struct.unpack("B", song_file.read(1)) + log.debug('length_descriptor: %d' % length_descriptor) + data = song_file.read(length_descriptor) + log.debug(data) + if block_key == TITLE: + self.title = self.decode(data) + elif block_key == AUTHOR: + authors = self.decode(data).split(" / ") + for author in authors: + if author.find(",") != -1: + author_parts = author.split(", ") + author = author_parts[1] + " " + author_parts[0] + self.parse_author(author) + elif block_key == COPYRIGHT: + self.add_copyright(self.decode(data)) + elif block_key == CCLI_NO: + # Try to get the CCLI number even if the field contains additional text + match = re.search(r'\d+', self.decode(data)) + if match: + self.ccli_number = int(match.group()) + else: + log.warning("Can't parse CCLI Number from string: {text}".format( + text=self.decode(data))) + elif block_key == VERSE: + self.add_verse(self.decode(data), "{tag}{number}".format( + tag=VerseType.tags[VerseType.Verse], number=verse_no)) + elif block_key == CHORUS: + self.add_verse(self.decode(data), "{tag}{number}".format( + tag=VerseType.tags[VerseType.Chorus], number=verse_no)) + elif block_key == BRIDGE: + self.add_verse(self.decode(data), "{tag}{number}".format( + tag=VerseType.tags[VerseType.Bridge], number=verse_no)) + elif block_key == TOPIC: + self.topics.append(self.decode(data)) + elif block_key == COMMENTS: + self.comments = self.decode(data) + elif block_key == VERSE_ORDER: + verse_tag = self.to_openlp_verse_tag(self.decode(data), True) + if verse_tag: + if not isinstance(verse_tag, str): + verse_tag = self.decode(verse_tag) + self.ssp_verse_order_list.append(verse_tag) + elif block_key == SONG_BOOK: + self.song_book_name = self.decode(data) + elif block_key == CUSTOM_VERSE: + verse_tag = self.to_openlp_verse_tag(verse_name) + self.add_verse(self.decode(data), verse_tag) + else: + log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data)) + song_file.seek(next_block_starts) + except struct.error: + log.exception('Unexpected data in {name}'.format(name=file_path)) + self.log_error(file_path, translate('SongsPlugin.SongShowPlusImport', + 'File is not a valid SongShowPlus file.')) + continue self.verse_order_list = self.ssp_verse_order_list if not self.finish(): self.log_error(file_path) diff --git a/openlp/plugins/songs/lib/importers/worshipassistant.py b/openlp/plugins/songs/lib/importers/worshipassistant.py index b3411a8ee..20d2b8fda 100644 --- a/openlp/plugins/songs/lib/importers/worshipassistant.py +++ b/openlp/plugins/songs/lib/importers/worshipassistant.py @@ -91,6 +91,10 @@ class WorshipAssistantImport(SongImport): translate('SongsPlugin.WorshipAssistantImport', 'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e)) return + except UnicodeDecodeError as e: + self.log_error(translate('SongsPlugin.WorshipAssistantImport', + 'Decoding error: {error}').format(error=e)) + return num_records = len(records) log.info('{count} records found in CSV file'.format(count=num_records)) self.import_wizard.progress_bar.setMaximum(num_records) @@ -103,11 +107,11 @@ class WorshipAssistantImport(SongImport): record = dict((field.upper(), value) for field, value in record.items()) # The CSV file has a line in the middle of the file where the headers are repeated. # We need to skip this line. - if record['TITLE'] == "TITLE" and record['AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2': - continue - self.set_defaults() - verse_order_list = [] try: + if record['TITLE'] == "TITLE" and record['AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2': + continue + self.set_defaults() + verse_order_list = [] self.title = record['TITLE'] if record['AUTHOR'] != EMPTY_STR: self.parse_author(record['AUTHOR']) @@ -128,6 +132,11 @@ class WorshipAssistantImport(SongImport): 'File not valid WorshipAssistant CSV format.'), 'TypeError: {error}'.format(error=e)) return + except KeyError as e: + self.log_error(translate('SongsPlugin.WorshipAssistantImport', + 'File not valid WorshipAssistant CSV format.'), + 'KeyError: {error}'.format(error=e)) + return verse = '' used_verses = [] verse_id = VerseType.tags[VerseType.Verse] + '1' diff --git a/openlp/plugins/songs/lib/importers/worshipcenterpro.py b/openlp/plugins/songs/lib/importers/worshipcenterpro.py index 2c4f38122..cff203a26 100644 --- a/openlp/plugins/songs/lib/importers/worshipcenterpro.py +++ b/openlp/plugins/songs/lib/importers/worshipcenterpro.py @@ -52,7 +52,7 @@ class WorshipCenterProImport(SongImport): try: conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};' 'DBQ={source}'.format(source=self.import_source)) - except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: + except Exception as e: log.warning('Unable to connect the WorshipCenter Pro ' 'database {source}. {error}'.format(source=self.import_source, error=str(e))) # Unfortunately no specific exception type diff --git a/openlp/plugins/songs/lib/importers/zionworx.py b/openlp/plugins/songs/lib/importers/zionworx.py index 023350cd3..42ab6cc5c 100644 --- a/openlp/plugins/songs/lib/importers/zionworx.py +++ b/openlp/plugins/songs/lib/importers/zionworx.py @@ -87,35 +87,39 @@ class ZionWorxImport(SongImport): num_records = len(records) log.info('{count} records found in CSV file'.format(count=num_records)) self.import_wizard.progress_bar.setMaximum(num_records) - for index, record in enumerate(records, 1): - if self.stop_import_flag: - return - self.set_defaults() - try: - self.title = record['Title1'] - if record['Title2']: - self.alternate_title = record['Title2'] - self.parse_author(record['Writer']) - self.add_copyright(record['Copyright']) - lyrics = record['Lyrics'] - except UnicodeDecodeError as e: - self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index), - translate('SongsPlugin.ZionWorxImport', 'Decoding error: {error}').format(error=e)) - continue - except TypeError as e: - self.log_error(translate('SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'), - 'TypeError: {error}'.format(error=e)) - return - verse = '' - for line in lyrics.splitlines(): - if line and not line.isspace(): - verse += line + '\n' - elif verse: + try: + for index, record in enumerate(records, 1): + if self.stop_import_flag: + return + self.set_defaults() + try: + self.title = record['Title1'] + if record['Title2']: + self.alternate_title = record['Title2'] + self.parse_author(record['Writer']) + self.add_copyright(record['Copyright']) + lyrics = record['Lyrics'] + except UnicodeDecodeError as e: + self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index), + translate('SongsPlugin.ZionWorxImport', + 'Decoding error: {error}').format(error=e)) + continue + except TypeError as e: + self.log_error(translate('SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'), + 'TypeError: {error}'.format(error=e)) + return + verse = '' + for line in lyrics.splitlines(): + if line and not line.isspace(): + verse += line + '\n' + elif verse: + self.add_verse(verse, 'v') + verse = '' + if verse: self.add_verse(verse, 'v') - verse = '' - if verse: - self.add_verse(verse, 'v') - title = self.title - if not self.finish(): - self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index + - (': "' + title + '"' if title else '')) + title = self.title + if not self.finish(): + self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index + + (': "' + title + '"' if title else '')) + except AttributeError: + self.log_error(translate('SongsPlugin.ZionWorxImport', 'Error reading CSV file.')) diff --git a/tests/functional/openlp_core/ui/test_style.py b/tests/functional/openlp_core/ui/test_style.py index fd346f4f7..32d8df998 100644 --- a/tests/functional/openlp_core/ui/test_style.py +++ b/tests/functional/openlp_core/ui/test_style.py @@ -51,13 +51,13 @@ def test_get_application_stylesheet_dark(mocked_qdarkstyle, MockSettings): @patch('openlp.core.ui.style.HAS_DARK_STYLE', False) @patch('openlp.core.ui.style.is_win') @patch('openlp.core.ui.style.Settings') -@patch('openlp.core.ui.style.Registry') -def test_get_application_stylesheet_not_alternate_rows(MockRegistry, MockSettings, mocked_is_win): +@patch('openlp.core.app.QtWidgets.QApplication.palette') +def test_get_application_stylesheet_not_alternate_rows(mocked_palette, MockSettings, mocked_is_win): """Test that the alternate rows stylesheet is returned when enabled in settings""" # GIVEN: We're on Windows and no dark style is set mocked_is_win.return_value = False MockSettings.return_value.value.return_value = False - MockRegistry.return_value.get.return_value.palette.return_value.color.return_value.name.return_value = 'color' + mocked_palette.return_value.color.return_value.name.return_value = 'color' # WHEN: can_show_icon() is called result = get_application_stylesheet() diff --git a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py index 603fa6b47..a6037c800 100644 --- a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py +++ b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py @@ -48,7 +48,8 @@ class TestWordProjectImport(TestCase): self.manager_patcher.start() @patch.object(Path, 'read_text') - def test_process_books(self, mocked_read_text): + @patch.object(Path, 'exists') + def test_process_books(self, mocked_exists, mocked_read_text): """ Test the process_books() method """ @@ -58,11 +59,14 @@ class TestWordProjectImport(TestCase): importer.stop_import_flag = False importer.language_id = 'en' mocked_read_text.return_value = INDEX_PAGE + mocked_exists.return_value = True # WHEN: process_books() is called - with patch.object(importer, 'find_and_create_book') as mocked_find_and_create_book, \ + with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ + patch.object(importer, 'find_and_create_book') as mocked_find_and_create_book, \ patch.object(importer, 'process_chapters') as mocked_process_chapters, \ patch.object(importer, 'session') as mocked_session: + mocked_unzip_file.return_value = True importer.process_books() # THEN: The right methods should have been called @@ -173,6 +177,8 @@ class TestWordProjectImport(TestCase): patch.object(importer, 'get_language_id') as mocked_get_language_id, \ patch.object(importer, 'process_books') as mocked_process_books, \ patch.object(importer, '_cleanup') as mocked_cleanup: + mocked_unzip_file.return_value = True + mocked_process_books.return_value = True mocked_get_language_id.return_value = 1 result = importer.do_import()