Made the bibel and song import more robust.

Tested the importers by loading a text file, a png file and an xml file.
This commit is contained in:
Tomas Groth 2020-01-18 21:45:47 +00:00 committed by Tim Bentley
parent 9dd8f51773
commit 1ebc6737d7
27 changed files with 393 additions and 209 deletions

View File

@ -29,7 +29,7 @@ test_script:
after_test: after_test:
# This is where we create a package using PyInstaller # This is where we create a package using PyInstaller
# Install 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 # 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 # - 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 # - PortableApps.comInstaller_3.4.4.paf.exe /S

View File

@ -306,7 +306,7 @@ class Settings(QtCore.QSettings):
'songs/db hostname': '', 'songs/db hostname': '',
'songs/db database': '', 'songs/db database': '',
'songs/last used search type': SongSearch.Entire, 'songs/last used search type': SongSearch.Entire,
'songs/last import type': None, 'songs/last import type': 0,
'songs/update service on edit': False, 'songs/update service on edit': False,
'songs/add song from service': True, 'songs/add song from service': True,
'songs/add songbook slide': False, 'songs/add songbook slide': False,

View File

@ -21,10 +21,9 @@
""" """
The :mod:`~openlp.core.ui.dark` module looks for and loads a dark theme 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 import is_win
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
@ -88,7 +87,7 @@ def get_application_stylesheet():
stylesheet = qdarkstyle.load_stylesheet_pyqt5() stylesheet = qdarkstyle.load_stylesheet_pyqt5()
else: else:
if not Settings().value('advanced/alternate rows'): 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 = \ alternate_rows_repair_stylesheet = \
'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
stylesheet += alternate_rows_repair_stylesheet stylesheet += alternate_rows_repair_stylesheet

View File

@ -40,6 +40,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib.db import delete_database from openlp.core.lib.db import delete_database
from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.ui import critical_error_message_box 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.edits import PathEdit
from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings
from openlp.plugins.bibles.lib.db import clean_filename 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_label.setObjectName('SwordFolderLabel')
self.sword_folder_path_edit = PathEdit( self.sword_folder_path_edit = PathEdit(
self.sword_folder_tab, self.sword_folder_tab,
path_type=PathEditType.Directories,
default_path=Settings().value('bibles/last directory import'), default_path=Settings().value('bibles/last directory import'),
dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD), dialog_caption=WizardStrings.OpenTypeFile.format(file_type=WizardStrings.SWORD),
show_revert=False, show_revert=False,
@ -502,6 +504,11 @@ class BibleImportForm(OpenLPWizard):
self.sword_folder_path_edit.setFocus() self.sword_folder_path_edit.setFocus()
return False return False
key = self.sword_bible_combo_box.itemData(self.sword_bible_combo_box.currentIndex()) 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]: if 'description' in self.pysword_folder_modules_json[key]:
self.version_name_edit.setText(self.pysword_folder_modules_json[key]['description']) self.version_name_edit.setText(self.pysword_folder_modules_json[key]['description'])
if 'distributionlicense' in self.pysword_folder_modules_json[key]: if 'distributionlicense' in self.pysword_folder_modules_json[key]:

View File

@ -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. All CSV files are expected to use a comma (',') as the delimiter and double quotes ('"') as the quote symbol.
""" """
import csv import csv
import logging
from collections import namedtuple from collections import namedtuple
from openlp.core.common import get_file_encoding 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.core.lib.exceptions import ValidationError
from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.bibleimport import BibleImport
log = logging.getLogger(__name__)
Book = namedtuple('Book', 'id, testament_id, name, abbreviation') Book = namedtuple('Book', 'id, testament_id, name, abbreviation')
Verse = namedtuple('Verse', 'book_id_name, chapter_number, number, text') 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: with file_path.open('r', encoding=encoding, newline='') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"') csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
return [results_tuple(*line) for line in csv_reader] 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)) raise ValidationError(msg='Parsing "{file}" failed'.format(file=file_path))
def process_books(self, books): def process_books(self, books):

View File

@ -22,10 +22,12 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from zipfile import ZipFile from zipfile import ZipFile, BadZipFile
from bs4 import BeautifulSoup, NavigableString, Tag 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 from openlp.plugins.bibles.lib.bibleimport import BibleImport
@ -50,18 +52,30 @@ class WordProjectBible(BibleImport):
Unzip the file to a temporary directory Unzip the file to a temporary directory
""" """
self.tmp = TemporaryDirectory() self.tmp = TemporaryDirectory()
try:
with ZipFile(self.file_path) as zip_file: with ZipFile(self.file_path) as zip_file:
zip_file.extractall(self.tmp.name) 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) self.base_path = Path(self.tmp.name, self.file_path.stem)
return True
def process_books(self): def process_books(self):
""" """
Extract and create the bible books from the parsed html Extract and create the bible books from the parsed html
:param bible_data: parsed xml :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') soup = BeautifulSoup(page, 'lxml')
bible_books = soup.find('div', 'textOptions').find_all('li') bible_books = soup.find('div', 'textOptions').find_all('li')
book_count = len(bible_books) 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) 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.process_chapters(db_book, book_id, book_link)
self.session.commit() self.session.commit()
return True
def process_chapters(self, db_book, book_id, book_link): def process_chapters(self, db_book, book_id, book_link):
""" """
@ -154,11 +169,11 @@ class WordProjectBible(BibleImport):
Loads a Bible from file. Loads a Bible from file.
""" """
self.log_debug('Starting WordProject import from "{name}"'.format(name=self.file_path)) 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)) self.language_id = self.get_language_id(None, bible_name=str(self.file_path))
result = False result = False
if self.language_id: if self.language_id:
self.process_books() result = self.process_books()
result = True
self._cleanup() self._cleanup()
return result return result

View File

@ -54,7 +54,12 @@ class ZefaniaBible(BibleImport):
if not language_id: if not language_id:
return False return False
no_of_books = int(xmlbible.xpath('count(//BIBLEBOOK)')) 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: for BIBLEBOOK in xmlbible:
if self.stop_import_flag: if self.stop_import_flag:
break break

View File

@ -396,7 +396,10 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name) dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name)
path_edit = PathEdit( path_edit = PathEdit(
parent=import_widget, path_type=path_type, dialog_caption=dialog_caption, show_revert=False) 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') path_edit.path = Settings().value(self.plugin.settings_section + '/last directory import')
file_path_layout.addWidget(path_edit) file_path_layout.addWidget(path_edit)
import_layout.addLayout(file_path_layout) import_layout.addLayout(file_path_layout)

View File

@ -66,11 +66,16 @@ class CCLIFileImport(SongImport):
except UnicodeDecodeError: except UnicodeDecodeError:
details = chardet.detect(detect_content) details = chardet.detect(detect_content)
in_file = codecs.open(file_path, 'r', details['encoding']) in_file = codecs.open(file_path, 'r', details['encoding'])
try:
if not in_file.read(1) == '\ufeff': if not in_file.read(1) == '\ufeff':
# not UTF or no BOM was found # not UTF or no BOM was found
in_file.seek(0) in_file.seek(0)
lines = in_file.readlines() lines = in_file.readlines()
in_file.close() in_file.close()
except UnicodeDecodeError:
self.log_error(file_path, translate('SongsPlugin.CCLIFileImport',
'The file contains unreadable characters.'))
continue
ext = file_path.suffix.lower() ext = file_path.suffix.lower()
if ext == '.usr' or ext == '.bin': if ext == '.usr' or ext == '.bin':
log.info('SongSelect USR format file found: {name}'.format(name=file_path)) log.info('SongSelect USR format file found: {name}'.format(name=file_path))

View File

@ -25,6 +25,7 @@ ChordPro files into the current database.
import logging import logging
import re import re
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.db import AuthorType from openlp.plugins.songs.lib.db import AuthorType
@ -60,7 +61,12 @@ class ChordProImport(SongImport):
""" """
self.set_defaults() self.set_defaults()
# Loop over the lines of the file # Loop over the lines of the file
try:
file_content = song_file.read() 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 = ''
current_verse_type = 'v' current_verse_type = 'v'
skip_block = False skip_block = False
@ -181,7 +187,7 @@ class ChordProImport(SongImport):
current_verse = re.sub(r'\[.*?\]', '', current_verse) current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type) self.add_verse(current_verse.rstrip(), current_verse_type)
# if no title was in directives, get it from the first line # 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] (verse_def, verse_text, lang) = self.verses[0]
# strip any chords from the title # strip any chords from the title
self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0]) self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0])

View File

@ -87,21 +87,30 @@ class DreamBeamImport(SongImport):
return return
self.set_defaults() self.set_defaults()
author_copyright = '' author_copyright = ''
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True, recover=True)
try: try:
with file_path.open('r') as xml_file: with file_path.open('r') as xml_file:
parsed_file = etree.parse(xml_file, parser) parsed_file = etree.parse(xml_file, parser)
except etree.XMLSyntaxError: 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) self.log_error(file_path, SongStrings.XMLSyntaxError)
continue 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) song_xml = objectify.fromstring(xml)
if song_xml.tag != 'DreamSong': if song_xml.tag != 'DreamSong':
self.log_error( self.log_error(
file_path, file_path,
translate('SongsPlugin.DreamBeamImport', translate('SongsPlugin.DreamBeamImport',
'Invalid DreamBeam song file_path. Missing DreamSong tag.')) 'Invalid DreamBeam song file. Missing DreamSong tag.'))
continue continue
if hasattr(song_xml, 'Version'): if hasattr(song_xml, 'Version'):
self.version = float(song_xml.Version.text) self.version = float(song_xml.Version.text)

View File

@ -25,9 +25,10 @@ import re
from lxml import etree, objectify from lxml import etree, objectify
from openlp.core.common import normalize_str 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 import VerseType
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -48,10 +49,32 @@ class EasySlidesImport(SongImport):
def do_import(self): def do_import(self):
log.info('Importing EasySlides XML file {source}'.format(source=self.import_source)) log.info('Importing EasySlides XML file {source}'.format(source=self.import_source))
parser = etree.XMLParser(remove_blank_text=True, recover=True) parser = etree.XMLParser(remove_blank_text=True, recover=True)
try:
with self.import_source.open('r') as xml_file:
parsed_file = etree.parse(str(self.import_source), parser) parsed_file = etree.parse(str(self.import_source), parser)
xml = etree.tostring(parsed_file).decode() 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) 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: for song in song_xml.Item:
if self.stop_import_flag: if self.stop_import_flag:
return return

View File

@ -78,12 +78,17 @@ class EasyWorshipSongImport(SongImport):
""" """
self.import_source = Path(self.import_source) self.import_source = Path(self.import_source)
ext = self.import_source.suffix.lower() ext = self.import_source.suffix.lower()
try:
if ext == '.ews': if ext == '.ews':
self.import_ews() self.import_ews()
elif ext == '.db': elif ext == '.db':
self.import_db() self.import_db()
else: else:
self.import_sqlite_db() 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): def import_ews(self):
""" """

View File

@ -127,6 +127,10 @@ class FoilPresenterImport(SongImport):
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
self.log_error(file_path, SongStrings.XMLSyntaxError) self.log_error(file_path, SongStrings.XMLSyntaxError)
log.exception('XML syntax error in file {path}'.format(path=file_path)) 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): class FoilPresenter(object):

View File

@ -130,6 +130,7 @@ class OpenLPSongImport(SongImport):
else: else:
has_authors_songs = False has_authors_songs = False
# Load up the tabls and map them out # Load up the tabls and map them out
try:
source_authors_table = source_meta.tables['authors'] source_authors_table = source_meta.tables['authors']
source_song_books_table = source_meta.tables['song_books'] source_song_books_table = source_meta.tables['song_books']
source_songs_table = source_meta.tables['songs'] source_songs_table = source_meta.tables['songs']
@ -137,6 +138,10 @@ class OpenLPSongImport(SongImport):
source_authors_songs_table = source_meta.tables['authors_songs'] source_authors_songs_table = source_meta.tables['authors_songs']
source_songs_topics_table = source_meta.tables['songs_topics'] source_songs_topics_table = source_meta.tables['songs_topics']
source_media_files_songs_table = None 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 # Set up media_files relations
if has_media_files: if has_media_files:
source_media_files_table = source_meta.tables['media_files'] source_media_files_table = source_meta.tables['media_files']

View File

@ -58,7 +58,7 @@ class OPSProImport(SongImport):
try: try:
conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};' conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};'
'PWD={password}'.format(source=self.import_source, password=password)) '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, log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source,
error=str(e))) error=str(e)))
# Unfortunately no specific exception type # Unfortunately no specific exception type

View File

@ -22,10 +22,16 @@
The :mod:`powerpraiseimport` module provides the functionality for importing The :mod:`powerpraiseimport` module provides the functionality for importing
Powerpraise song files into the current database. 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.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__)
class PowerPraiseImport(SongImport): class PowerPraiseImport(SongImport):
@ -40,11 +46,21 @@ class PowerPraiseImport(SongImport):
return return
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name)) self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
with file_path.open('rb') as xml_file: with file_path.open('rb') as xml_file:
try:
root = objectify.parse(xml_file).getroot() root = objectify.parse(xml_file).getroot()
self.process_song(root) 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.set_defaults()
try:
self.title = str(root.general.title) self.title = str(root.general.title)
verse_order_list = [] verse_order_list = []
verse_count = {} verse_count = {}
@ -79,4 +95,7 @@ class PowerPraiseImport(SongImport):
self.verse_order_list = verse_order_list self.verse_order_list = verse_order_list
if not self.finish(): if not self.finish():
self.log_error(self.import_source) self.log_error(file_path)
except AttributeError:
self.log_error(file_path, translate('SongsPlugin.PowerPraiseImport',
'Invalid PowerPraise song file. Missing needed tag.'))

View File

@ -22,6 +22,7 @@
The :mod:`presentationmanager` module provides the functionality for importing The :mod:`presentationmanager` module provides the functionality for importing
Presentationmanager song files into the current database. Presentationmanager song files into the current database.
""" """
import logging
import re import re
from lxml import etree, objectify 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.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
log = logging.getLogger(__name__)
class PresentationManagerImport(SongImport): class PresentationManagerImport(SongImport):
""" """
@ -54,12 +57,24 @@ class PresentationManagerImport(SongImport):
try: try:
tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) tree = etree.fromstring(text, parser=etree.XMLParser(recover=True))
except ValueError: except ValueError:
log.exception('XML syntax error in file {name}'.format(name=file_path))
self.log_error(file_path, self.log_error(file_path,
translate('SongsPlugin.PresentationManagerImport', translate('SongsPlugin.PresentationManagerImport',
'File is not in XML-format, which is the only format supported.')) 'File is not in XML-format, which is the only format supported.'))
continue continue
root = objectify.fromstring(etree.tostring(tree)) 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) 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): def _get_attr(self, elem, name):
""" """

View File

@ -25,11 +25,13 @@ ProPresenter song files into the current installation database.
import base64 import base64
import logging 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.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import strip_rtf from openlp.plugins.songs.lib import strip_rtf
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -48,8 +50,23 @@ class ProPresenterImport(SongImport):
self.import_wizard.increment_progress_bar( self.import_wizard.increment_progress_bar(
WizardStrings.ImportingType.format(source=file_path.name)) WizardStrings.ImportingType.format(source=file_path.name))
with file_path.open('rb') as xml_file: with file_path.open('rb') as xml_file:
try:
root = objectify.parse(xml_file).getroot() 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) 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): def process_song(self, root, file_path):
""" """
@ -62,7 +79,7 @@ class ProPresenterImport(SongImport):
# Extract ProPresenter versionNumber # Extract ProPresenter versionNumber
try: try:
self.version = int(root.get('versionNumber')) self.version = int(root.get('versionNumber'))
except ValueError: except (ValueError, TypeError):
log.debug('ProPresenter versionNumber invalid or missing') log.debug('ProPresenter versionNumber invalid or missing')
return return

View File

@ -29,6 +29,7 @@ import re
from pathlib import Path from pathlib import Path
from openlp.core.common import get_file_encoding, is_macosx, is_win 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.core.common.settings import Settings
from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib.importers.songimport import SongImport 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'): if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'):
self.input_file_encoding = 'cp1252' self.input_file_encoding = 'cp1252'
with file_path.open(encoding=self.input_file_encoding) as song_file: with file_path.open(encoding=self.input_file_encoding) as song_file:
try:
song_data = song_file.readlines() 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: else:
continue continue
self.title = file_path.stem self.title = file_path.stem

View File

@ -22,12 +22,16 @@
The :mod:`songpro` module provides the functionality for importing SongPro The :mod:`songpro` module provides the functionality for importing SongPro
songs into the OpenLP database. songs into the OpenLP database.
""" """
import logging
import re import re
from pathlib import Path from pathlib import Path
from openlp.core.common.i18n import translate
from openlp.plugins.songs.lib import strip_rtf from openlp.plugins.songs.lib import strip_rtf
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
log = logging.getLogger(__name__)
class SongProImport(SongImport): class SongProImport(SongImport):
""" """
@ -82,7 +86,13 @@ class SongProImport(SongImport):
break break
file_text = file_line.rstrip() file_text = file_line.rstrip()
if file_text and file_text[0] == '#': if file_text and file_text[0] == '#':
try:
self.process_section(tag, text.rstrip()) 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:] tag = file_text[1:]
text = '' text = ''
else: else:

View File

@ -26,6 +26,7 @@ import logging
import re import re
import struct import struct
from openlp.core.common.i18n import translate
from openlp.core.widgets.wizard import WizardStrings from openlp.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding
from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.importers.songimport import SongImport
@ -100,6 +101,7 @@ class SongShowPlusImport(SongImport):
self.other_list = {} self.other_list = {}
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name), 0) self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name), 0)
with file_path.open('rb') as song_file: with file_path.open('rb') as song_file:
try:
while True: while True:
block_key, = struct.unpack("I", song_file.read(4)) block_key, = struct.unpack("I", song_file.read(4))
log.debug('block_key: %d' % block_key) log.debug('block_key: %d' % block_key)
@ -150,16 +152,17 @@ class SongShowPlusImport(SongImport):
if match: if match:
self.ccli_number = int(match.group()) self.ccli_number = int(match.group())
else: else:
log.warning("Can't parse CCLI Number from string: {text}".format(text=self.decode(data))) log.warning("Can't parse CCLI Number from string: {text}".format(
text=self.decode(data)))
elif block_key == VERSE: elif block_key == VERSE:
self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Verse], self.add_verse(self.decode(data), "{tag}{number}".format(
number=verse_no)) tag=VerseType.tags[VerseType.Verse], number=verse_no))
elif block_key == CHORUS: elif block_key == CHORUS:
self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Chorus], self.add_verse(self.decode(data), "{tag}{number}".format(
number=verse_no)) tag=VerseType.tags[VerseType.Chorus], number=verse_no))
elif block_key == BRIDGE: elif block_key == BRIDGE:
self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Bridge], self.add_verse(self.decode(data), "{tag}{number}".format(
number=verse_no)) tag=VerseType.tags[VerseType.Bridge], number=verse_no))
elif block_key == TOPIC: elif block_key == TOPIC:
self.topics.append(self.decode(data)) self.topics.append(self.decode(data))
elif block_key == COMMENTS: elif block_key == COMMENTS:
@ -178,6 +181,11 @@ class SongShowPlusImport(SongImport):
else: else:
log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data)) log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data))
song_file.seek(next_block_starts) 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 self.verse_order_list = self.ssp_verse_order_list
if not self.finish(): if not self.finish():
self.log_error(file_path) self.log_error(file_path)

View File

@ -91,6 +91,10 @@ class WorshipAssistantImport(SongImport):
translate('SongsPlugin.WorshipAssistantImport', translate('SongsPlugin.WorshipAssistantImport',
'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e)) 'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e))
return return
except UnicodeDecodeError as e:
self.log_error(translate('SongsPlugin.WorshipAssistantImport',
'Decoding error: {error}').format(error=e))
return
num_records = len(records) num_records = len(records)
log.info('{count} records found in CSV file'.format(count=num_records)) log.info('{count} records found in CSV file'.format(count=num_records))
self.import_wizard.progress_bar.setMaximum(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()) 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. # The CSV file has a line in the middle of the file where the headers are repeated.
# We need to skip this line. # We need to skip this line.
try:
if record['TITLE'] == "TITLE" and record['AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2': if record['TITLE'] == "TITLE" and record['AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2':
continue continue
self.set_defaults() self.set_defaults()
verse_order_list = [] verse_order_list = []
try:
self.title = record['TITLE'] self.title = record['TITLE']
if record['AUTHOR'] != EMPTY_STR: if record['AUTHOR'] != EMPTY_STR:
self.parse_author(record['AUTHOR']) self.parse_author(record['AUTHOR'])
@ -128,6 +132,11 @@ class WorshipAssistantImport(SongImport):
'File not valid WorshipAssistant CSV format.'), 'File not valid WorshipAssistant CSV format.'),
'TypeError: {error}'.format(error=e)) 'TypeError: {error}'.format(error=e))
return return
except KeyError as e:
self.log_error(translate('SongsPlugin.WorshipAssistantImport',
'File not valid WorshipAssistant CSV format.'),
'KeyError: {error}'.format(error=e))
return
verse = '' verse = ''
used_verses = [] used_verses = []
verse_id = VerseType.tags[VerseType.Verse] + '1' verse_id = VerseType.tags[VerseType.Verse] + '1'

View File

@ -52,7 +52,7 @@ class WorshipCenterProImport(SongImport):
try: try:
conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};' conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};'
'DBQ={source}'.format(source=self.import_source)) '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 ' log.warning('Unable to connect the WorshipCenter Pro '
'database {source}. {error}'.format(source=self.import_source, error=str(e))) 'database {source}. {error}'.format(source=self.import_source, error=str(e)))
# Unfortunately no specific exception type # Unfortunately no specific exception type

View File

@ -87,6 +87,7 @@ class ZionWorxImport(SongImport):
num_records = len(records) num_records = len(records)
log.info('{count} records found in CSV file'.format(count=num_records)) log.info('{count} records found in CSV file'.format(count=num_records))
self.import_wizard.progress_bar.setMaximum(num_records) self.import_wizard.progress_bar.setMaximum(num_records)
try:
for index, record in enumerate(records, 1): for index, record in enumerate(records, 1):
if self.stop_import_flag: if self.stop_import_flag:
return return
@ -100,7 +101,8 @@ class ZionWorxImport(SongImport):
lyrics = record['Lyrics'] lyrics = record['Lyrics']
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index), self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index),
translate('SongsPlugin.ZionWorxImport', 'Decoding error: {error}').format(error=e)) translate('SongsPlugin.ZionWorxImport',
'Decoding error: {error}').format(error=e))
continue continue
except TypeError as e: except TypeError as e:
self.log_error(translate('SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'), self.log_error(translate('SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'),
@ -119,3 +121,5 @@ class ZionWorxImport(SongImport):
if not self.finish(): if not self.finish():
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index + self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index +
(': "' + title + '"' if title else '')) (': "' + title + '"' if title else ''))
except AttributeError:
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Error reading CSV file.'))

View File

@ -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.HAS_DARK_STYLE', False)
@patch('openlp.core.ui.style.is_win') @patch('openlp.core.ui.style.is_win')
@patch('openlp.core.ui.style.Settings') @patch('openlp.core.ui.style.Settings')
@patch('openlp.core.ui.style.Registry') @patch('openlp.core.app.QtWidgets.QApplication.palette')
def test_get_application_stylesheet_not_alternate_rows(MockRegistry, MockSettings, mocked_is_win): 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""" """Test that the alternate rows stylesheet is returned when enabled in settings"""
# GIVEN: We're on Windows and no dark style is set # GIVEN: We're on Windows and no dark style is set
mocked_is_win.return_value = False mocked_is_win.return_value = False
MockSettings.return_value.value.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 # WHEN: can_show_icon() is called
result = get_application_stylesheet() result = get_application_stylesheet()

View File

@ -48,7 +48,8 @@ class TestWordProjectImport(TestCase):
self.manager_patcher.start() self.manager_patcher.start()
@patch.object(Path, 'read_text') @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 Test the process_books() method
""" """
@ -58,11 +59,14 @@ class TestWordProjectImport(TestCase):
importer.stop_import_flag = False importer.stop_import_flag = False
importer.language_id = 'en' importer.language_id = 'en'
mocked_read_text.return_value = INDEX_PAGE mocked_read_text.return_value = INDEX_PAGE
mocked_exists.return_value = True
# WHEN: process_books() is called # 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, 'process_chapters') as mocked_process_chapters, \
patch.object(importer, 'session') as mocked_session: patch.object(importer, 'session') as mocked_session:
mocked_unzip_file.return_value = True
importer.process_books() importer.process_books()
# THEN: The right methods should have been called # 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, 'get_language_id') as mocked_get_language_id, \
patch.object(importer, 'process_books') as mocked_process_books, \ patch.object(importer, 'process_books') as mocked_process_books, \
patch.object(importer, '_cleanup') as mocked_cleanup: 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 mocked_get_language_id.return_value = 1
result = importer.do_import() result = importer.do_import()