Importer cleanups

This commit is contained in:
Tim Bentley 2014-03-06 20:40:08 +00:00
parent 75281b5cf1
commit ef97095399
32 changed files with 391 additions and 416 deletions

View File

@ -2485,7 +2485,7 @@ $.mobile.transitionFallbacks = {};
return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' );
},
//return the substring of a filepath before the sub-page key, for making a server request
//return the substring of a file_path before the sub-page key, for making a server request
getFilePath: function( path ) {
var splitkey = '&' + $.mobile.subPageUrlKey;
return path && path.split( splitkey )[0].split( dialogHashKey )[0];

View File

@ -334,7 +334,7 @@ class SongImportForm(OpenLPWizard):
def perform_wizard(self):
"""
Perform the actual import. This method pulls in the correct importer class, and then runs the ``doImport``
Perform the actual import. This method pulls in the correct importer class, and then runs the ``do_import``
method of the importer to do the actual importing.
"""
source_format = self.current_format
@ -349,7 +349,7 @@ class SongImportForm(OpenLPWizard):
importer = self.plugin.import_songs(
source_format,
filenames=self.get_list_of_files(self.format_widgets[source_format]['file_list_widget']))
importer.doImport()
importer.do_import()
self.progress_label.setText(WizardStrings.FinishedImport)
def on_error_copy_to_button_clicked(self):

View File

@ -197,11 +197,8 @@ class VerseType(object):
"""
Return the translated UPPERCASE tag for a given tag, used to show translated verse tags in UI
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -217,11 +214,8 @@ class VerseType(object):
"""
Return the translated name for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -237,11 +231,8 @@ class VerseType(object):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
@ -257,11 +248,8 @@ class VerseType(object):
"""
Return the VerseType for a given tag
``verse_tag``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.translated_tags):
@ -277,11 +265,8 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_name: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
verse_name = verse_name.lower()
for num, name in enumerate(VerseType.names):
@ -294,8 +279,7 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
:param verse_name: The string to return a VerseType for
"""
verse_name = verse_name.lower()
for num, translation in enumerate(VerseType.translated_names):
@ -307,11 +291,8 @@ class VerseType(object):
"""
Return the VerseType for a given string
``verse_name``
The string to return a VerseType for
``default``
Default return value if no matching tag is found
:param verse_name: The string to return a VerseType for
:param default: Default return value if no matching tag is found
"""
if len(verse_name) > 1:
verse_index = VerseType.from_translated_string(verse_name)
@ -331,11 +312,11 @@ def retrieve_windows_encoding(recommendation=None):
Determines which encoding to use on an information source. The process uses both automated detection, which is
passed to this method as a recommendation, and user confirmation to return an encoding.
``recommendation``
A recommended encoding discovered programmatically for the user to confirm.
:param recommendation: A recommended encoding discovered programmatically for the user to confirm.
"""
# map chardet result to compatible windows standard code page
codepage_mapping = {'IBM866': 'cp866', 'TIS-620': 'cp874',
codepage_mapping = {
'IBM866': 'cp866', 'TIS-620': 'cp874',
'SHIFT_JIS': 'cp932', 'GB2312': 'cp936', 'HZ-GB-2312': 'cp936',
'EUC-KR': 'cp949', 'Big5': 'cp950', 'ISO-8859-2': 'cp1250',
'windows-1250': 'cp1250', 'windows-1251': 'cp1251',
@ -346,7 +327,8 @@ def retrieve_windows_encoding(recommendation=None):
recommendation = codepage_mapping[recommendation]
# Show dialog for encoding selection
encodings = [('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
encodings = [
('cp1256', translate('SongsPlugin', 'Arabic (CP-1256)')),
('cp1257', translate('SongsPlugin', 'Baltic (CP-1257)')),
('cp1250', translate('SongsPlugin', 'Central European (CP-1250)')),
('cp1251', translate('SongsPlugin', 'Cyrillic (CP-1251)')),
@ -367,17 +349,17 @@ def retrieve_windows_encoding(recommendation=None):
recommended_index = index
break
if recommended_index > -1:
choice = QtGui.QInputDialog.getItem(None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'The codepage setting is responsible\n'
'for the correct character representation.\nUsually you are fine with the preselected choice.'),
choice = QtGui.QInputDialog.getItem(
None, translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'The codepage setting is responsible\nfor the correct character '
'representation.\nUsually you are fine with the preselected choice.'),
[pair[1] for pair in encodings], recommended_index, False)
else:
choice = QtGui.QInputDialog.getItem(None,
translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'Please choose the character encoding.\n'
'The encoding is responsible for the correct character representation.'),
[pair[1] for pair in encodings], 0, False)
choice = QtGui.QInputDialog.getItem(
None, translate('SongsPlugin', 'Character Encoding'),
translate('SongsPlugin', 'Please choose the character encoding.\nThe encoding is responsible for the '
'correct character representation.'),
[pair[1] for pair in encodings], 0, False)
if not choice[1]:
return None
return next(filter(lambda item: item[1] == choice[0], encodings))[0]
@ -402,11 +384,8 @@ def clean_song(manager, song):
Cleans the search title, rebuilds the search lyrics, adds a default author if the song does not have one and other
clean ups. This should always called when a new song is added or changed.
``manager``
The song's manager.
``song``
The song object.
:param manager: The song's manager.
:param song: The song object.
"""
from .xml import SongXML
@ -425,8 +404,7 @@ def clean_song(manager, song):
# keeps the database clean. This can be removed when everybody has cleaned his songs.
song.lyrics = song.lyrics.replace('<lyrics language="en">', '<lyrics>')
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1])
for verse in verses])
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# We need a new and clean SongXML instance.
sxml = SongXML()
# Rebuild the song's verses, to remove any wrong verse names (for example translated ones), which might have
@ -466,8 +444,7 @@ def clean_song(manager, song):
break
else:
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1])
for verse in verses])
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one.
if not song.authors:
@ -484,17 +461,10 @@ def get_encoding(font, font_table, default_encoding, failed=False):
"""
Finds an encoding to use. Asks user, if necessary.
``font``
The number of currently active font.
``font_table``
Dictionary of fonts and respective encodings.
``default_encoding``
The default encoding to use when font_table is empty or no font is used.
``failed``
A boolean indicating whether the previous encoding didn't work.
:param font: The number of currently active font.
:param font_table: Dictionary of fonts and respective encodings.
:param default_encoding: The default encoding to use when font_table is empty or no font is used.
:param failed: A boolean indicating whether the previous encoding didn't work.
"""
encoding = None
if font in font_table:
@ -513,13 +483,11 @@ def strip_rtf(text, default_encoding=None):
This function strips RTF control structures and returns an unicode string.
Thanks to Markus Jarderot (MizardX) for this code, used by permission.
http://stackoverflow.com/questions/188545
``text``
RTF-encoded text, a string.
``default_encoding``
Default encoding to use when no encoding is specified.
:param text: RTF-encoded text, a string.
:param default_encoding: Default encoding to use when no encoding is specified.
"""
# Current font is the font tag we last met.
font = ''
@ -623,11 +591,8 @@ def delete_song(song_id, song_plugin):
Deletes a song from the database. Media files associated to the song
are removed prior to the deletion of the song.
``song_id``
The ID of the song to delete.
``song_plugin``
The song plugin instance.
:param song_id: The ID of the song to delete.
:param song_plugin: The song plugin instance.
"""
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
for media_file in media_files:

View File

@ -41,24 +41,20 @@ log = logging.getLogger(__name__)
class CCLIFileImport(SongImport):
"""
The :class:`CCLIFileImport` class provides OpenLP with the ability to import
CCLI SongSelect song files in both .txt and .usr formats. See
`<http://www.ccli.com/>`_ for more details.
The :class:`CCLIFileImport` class provides OpenLP with the ability to import CCLI SongSelect song files in
both .txt and .usr formats. See `<http://www.ccli.com/>`_ for more details.
"""
def __init__(self, manager, **kwargs):
"""
Initialise the import.
``manager``
The song manager for the running OpenLP installation.
``filenames``
The files to be imported.
:param manager: The song manager for the running OpenLP installation.
:param kwargs: The files to be imported.
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Import either a ``.usr`` or a ``.txt`` SongSelect file.
"""
@ -86,11 +82,11 @@ class CCLIFileImport(SongImport):
ext = os.path.splitext(filename)[1]
if ext.lower() == '.usr':
log.info('SongSelect .usr format file found: %s', filename)
if not self.doImportUsrFile(lines):
if not self.do_import_usr_file(lines):
self.log_error(filename)
elif ext.lower() == '.txt':
log.info('SongSelect .txt format file found: %s', filename)
if not self.doImportTxtFile(lines):
if not self.do_import_txt_file(lines):
self.log_error(filename)
else:
self.log_error(filename,
@ -99,14 +95,11 @@ class CCLIFileImport(SongImport):
if self.stop_import_flag:
return
def doImportUsrFile(self, textList):
def do_import_usr_file(self, text_list):
"""
The :func:`doImport_usr_file` method provides OpenLP with the ability
to import CCLI SongSelect songs in *USR* file format.
``textList``
An array of strings containing the usr file content.
**SongSelect .usr file format**
``[File]``
@ -156,11 +149,12 @@ class CCLIFileImport(SongImport):
*Fields* description
e.g. *Words=Above all powers....* [/n = CR, /n/t = CRLF]
:param text_list: An array of strings containing the usr file content.
"""
log.debug('USR file text: %s', textList)
log.debug('USR file text: %s', text_list)
song_author = ''
song_topics = ''
for line in textList:
for line in text_list:
if line.startswith('[S '):
ccli, line = line.split(']', 1)
if ccli.startswith('[S A'):
@ -177,7 +171,7 @@ class CCLIFileImport(SongImport):
song_topics = line[7:].strip().replace(' | ', '/t')
elif line.startswith('Fields='):
# Fields contain single line indicating verse, chorus, etc,
# /t delimited, same as with words field. store seperately
# /t delimited, same as with words field. store separately
# and process at end.
song_fields = line[7:].strip()
elif line.startswith('Words='):
@ -228,13 +222,12 @@ class CCLIFileImport(SongImport):
self.topics = [topic.strip() for topic in song_topics.split('/t')]
return self.finish()
def doImportTxtFile(self, textList):
def do_import_txt_file(self, text_list):
"""
The :func:`doImport_txt_file` method provides OpenLP with the ability
to import CCLI SongSelect songs in *TXT* file format.
``textList``
An array of strings containing the txt file content.
:param text_list: An array of strings containing the txt file content.
SongSelect .txt file format::
@ -260,13 +253,13 @@ class CCLIFileImport(SongImport):
# e.g. CCL-Liedlizenznummer: 14 / CCLI License No. 14
"""
log.debug('TXT file text: %s', textList)
log.debug('TXT file text: %s', text_list)
line_number = 0
check_first_verse_line = False
verse_text = ''
song_author = ''
verse_start = False
for line in textList:
for line in text_list:
clean_line = line.strip()
if not clean_line:
if line_number == 0:

View File

@ -87,6 +87,7 @@ class Topic(BaseModel):
"""
pass
def init_schema(url):
"""
Setup the songs database connection and initialise the database schema.
@ -183,8 +184,7 @@ def init_schema(url):
# Definition of the "media_files" table
media_files_table = Table('media_files', metadata,
Column('id', types.Integer(), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'),
default=None),
Column('song_id', types.Integer(), ForeignKey('songs.id'), default=None),
Column('file_name', types.Unicode(255), nullable=False),
Column('type', types.Unicode(64), nullable=False, default='audio'),
Column('weight', types.Integer(), default=0)
@ -200,8 +200,7 @@ def init_schema(url):
# Definition of the "songs" table
songs_table = Table('songs', metadata,
Column('id', types.Integer(), primary_key=True),
Column('song_book_id', types.Integer(),
ForeignKey('song_books.id'), default=None),
Column('song_book_id', types.Integer(), ForeignKey('song_books.id'), default=None),
Column('title', types.Unicode(255), nullable=False),
Column('alternate_title', types.Unicode(255)),
Column('lyrics', types.UnicodeText, nullable=False),
@ -214,8 +213,7 @@ def init_schema(url):
Column('search_title', types.Unicode(255), index=True, nullable=False),
Column('search_lyrics', types.UnicodeText, nullable=False),
Column('create_date', types.DateTime(), default=func.now()),
Column('last_modified', types.DateTime(), default=func.now(),
onupdate=func.now()),
Column('last_modified', types.DateTime(), default=func.now(), onupdate=func.now()),
Column('temporary', types.Boolean(), default=False)
)
@ -227,18 +225,14 @@ def init_schema(url):
# Definition of the "authors_songs" table
authors_songs_table = Table('authors_songs', metadata,
Column('author_id', types.Integer(),
ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(),
ForeignKey('songs.id'), primary_key=True)
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True)
)
# Definition of the "songs_topics" table
songs_topics_table = Table('songs_topics', metadata,
Column('song_id', types.Integer(),
ForeignKey('songs.id'), primary_key=True),
Column('topic_id', types.Integer(),
ForeignKey('topics.id'), primary_key=True)
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True)
)
mapper(Author, authors_table)
@ -246,13 +240,10 @@ def init_schema(url):
mapper(MediaFile, media_files_table)
mapper(Song, songs_table,
properties={
'authors': relation(Author, backref='songs',
secondary=authors_songs_table, lazy=False),
'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False),
'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs',
order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs',
secondary=songs_topics_table)
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table)
})
mapper(Topic, topics_table)

View File

@ -40,6 +40,7 @@ from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__)
class DreamBeamImport(SongImport):
"""
The :class:`DreamBeamImport` class provides the ability to import song files from
@ -83,7 +84,7 @@ class DreamBeamImport(SongImport):
* \*.xml
"""
def doImport(self):
def do_import(self):
"""
Receive a single file or a list of files to import.
"""
@ -103,7 +104,8 @@ class DreamBeamImport(SongImport):
xml = etree.tostring(parsed_file).decode()
song_xml = objectify.fromstring(xml)
if song_xml.tag != 'DreamSong':
self.log_error(file,
self.log_error(
file,
translate('SongsPlugin.DreamBeamImport', 'Invalid DreamBeam song file. Missing DreamSong tag.'))
continue
if hasattr(song_xml, 'Version'):
@ -127,9 +129,9 @@ class DreamBeamImport(SongImport):
if hasattr(song_xml, 'Number'):
self.song_number = str(song_xml.Number.text)
if hasattr(song_xml, 'Sequence'):
for LyricsSequenceItem in (song_xml.Sequence.iterchildren()):
self.verse_order_list.append("%s%s" % (LyricsSequenceItem.get('Type')[:1],
LyricsSequenceItem.get('Number')))
for lyrics_sequence_item in (song_xml.Sequence.iterchildren()):
self.verse_order_list.append("%s%s" % (lyrics_sequence_item.get('Type')[:1],
lyrics_sequence_item.get('Number')))
if hasattr(song_xml, 'Notes'):
self.comments = str(song_xml.Notes.text)
else:

View File

@ -37,6 +37,7 @@ from openlp.plugins.songs.lib.songimport import SongImport
log = logging.getLogger(__name__)
class EasySlidesImport(SongImport):
"""
Import songs exported from EasySlides
@ -50,7 +51,7 @@ class EasySlidesImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
log.info('Importing EasySlides XML file %s', self.import_source)
parser = etree.XMLParser(remove_blank_text=True)
parsed_file = etree.parse(self.import_source, parser)
@ -60,9 +61,9 @@ class EasySlidesImport(SongImport):
for song in song_xml.Item:
if self.stop_import_flag:
return
self._parseSong(song)
self._parse_song(song)
def _parseSong(self, song):
def _parse_song(self, song):
self._success = True
self._add_unicode_attribute('title', song.Title1, True)
if hasattr(song, 'Title2'):
@ -71,7 +72,7 @@ class EasySlidesImport(SongImport):
self._add_unicode_attribute('song_number', song.SongNumber)
if self.song_number == '0':
self.song_number = ''
self._addAuthors(song)
self._add_authors(song)
if hasattr(song, 'Copyright'):
self._add_copyright(song.Copyright)
if hasattr(song, 'LicenceAdmin1'):
@ -80,7 +81,7 @@ class EasySlidesImport(SongImport):
self._add_copyright(song.LicenceAdmin2)
if hasattr(song, 'BookReference'):
self._add_unicode_attribute('song_book_name', song.BookReference)
self._parseAndAddLyrics(song)
self._parse_and_add_lyrics(song)
if self._success:
if not self.finish():
self.log_error(song.Title1 if song.Title1 else '')
@ -94,14 +95,9 @@ class EasySlidesImport(SongImport):
present _success is set to False so the importer can react
appropriately.
``self_attribute``
The attribute in the song model to populate.
``import_attribute``
The imported value to convert to unicode and save to the song.
``mandatory``
Signals that this attribute must exist in a valid song.
:param self_attribute: The attribute in the song model to populate.
:param import_attribute: The imported value to convert to unicode and save to the song.
:param mandatory: Signals that this attribute must exist in a valid song.
"""
try:
setattr(self, self_attribute, str(import_attribute).strip())
@ -113,7 +109,7 @@ class EasySlidesImport(SongImport):
if mandatory:
self._success = False
def _addAuthors(self, song):
def _add_authors(self, song):
try:
authors = str(song.Writer).split(',')
self.authors = [author.strip() for author in authors if author.strip()]
@ -125,11 +121,9 @@ class EasySlidesImport(SongImport):
def _add_copyright(self, element):
"""
Add a piece of copyright to the total copyright information for the
song.
Add a piece of copyright to the total copyright information for the song.
``element``
The imported variable to get the data from.
:param element: The imported variable to get the data from.
"""
try:
self.add_copyright(str(element).strip())
@ -139,7 +133,12 @@ class EasySlidesImport(SongImport):
except AttributeError:
pass
def _parseAndAddLyrics(self, song):
def _parse_and_add_lyrics(self, song):
"""
Process the song lyrics
:param song: The song details
"""
try:
lyrics = str(song.Contents).strip()
except UnicodeDecodeError:
@ -151,29 +150,29 @@ class EasySlidesImport(SongImport):
lines = lyrics.split('\n')
# we go over all lines first, to determine information,
# which tells us how to parse verses later
regionlines = {}
separatorlines = 0
region_lines = {}
separator_lines = 0
for line in lines:
line = line.strip()
if not line:
continue
elif line[1:7] == 'region':
# this is region separator, probably [region 2]
region = self._extractRegion(line)
regionlines[region] = 1 + regionlines.get(region, 0)
region = self._extract_region(line)
region_lines[region] = 1 + region_lines.get(region, 0)
elif line[0] == '[':
separatorlines += 1
separator_lines += 1
# if the song has separators
separators = (separatorlines > 0)
separators = (separator_lines > 0)
# the number of different regions in song - 1
if len(regionlines) > 1:
if len(region_lines) > 1:
log.info('EasySlidesImport: the file contained a song named "%s"'
'with more than two regions, but only two regions are tested, encountered regions were: %s',
self.title, ','.join(list(regionlines.keys())))
'with more than two regions, but only two regions are tested, encountered regions were: %s',
self.title, ','.join(list(region_lines.keys())))
# if the song has regions
regions = (len(regionlines) > 0)
regions = (len(region_lines) > 0)
# if the regions are inside verses
regionsInVerses = (regions and regionlines[list(regionlines.keys())[0]] > 1)
regions_in_verses = (regions and region_lines[list(region_lines.keys())[0]] > 1)
MarkTypes = {
'CHORUS': VerseType.tags[VerseType.Chorus],
'VERSE': VerseType.tags[VerseType.Verse],
@ -185,21 +184,20 @@ class EasySlidesImport(SongImport):
verses = {}
# list as [region, versetype, versenum, instance]
our_verse_order = []
defaultregion = '1'
reg = defaultregion
default_region = '1'
reg = default_region
verses[reg] = {}
# instance differentiates occurrences of same verse tag
vt = 'V'
vn = '1'
inst = 1
for line in lines:
line = line.strip()
if not line:
if separators:
# separators are used, so empty line means slide break
# inside verse
if self._listHas(verses, [reg, vt, vn, inst]):
if self._list_has(verses, [reg, vt, vn, inst]):
inst += 1
else:
# separators are not used, so empty line starts a new verse
@ -207,9 +205,9 @@ class EasySlidesImport(SongImport):
vn = len(verses[reg].get(vt, {})) + 1
inst = 1
elif line[0:7] == '[region':
reg = self._extractRegion(line)
reg = self._extract_region(line)
verses.setdefault(reg, {})
if not regionsInVerses:
if not regions_in_verses:
vt = 'V'
vn = '1'
inst = 1
@ -224,10 +222,10 @@ class EasySlidesImport(SongImport):
marker = match.group(1).strip()
vn = match.group(2)
vt = MarkTypes.get(marker, 'O') if marker else 'V'
if regionsInVerses:
region = defaultregion
if regions_in_verses:
region = default_region
inst = 1
if self._listHas(verses, [reg, vt, vn, inst]):
if self._list_has(verses, [reg, vt, vn, inst]):
inst = len(verses[reg][vt][vn]) + 1
else:
if not [reg, vt, vn, inst] in our_verse_order:
@ -237,19 +235,17 @@ class EasySlidesImport(SongImport):
verses[reg][vt][vn].setdefault(inst, [])
verses[reg][vt][vn][inst].append(self.tidy_text(line))
# done parsing
versetags = []
# we use our_verse_order to ensure, we insert lyrics in the same order
# as these appeared originally in the file
for [reg, vt, vn, inst] in our_verse_order:
if self._listHas(verses, [reg, vt, vn, inst]):
if self._list_has(verses, [reg, vt, vn, inst]):
# this is false, but needs user input
lang = None
versetag = '%s%s' % (vt, vn)
versetags.append(versetag)
lines = '\n'.join(verses[reg][vt][vn][inst])
self.verses.append([versetag, lines, lang])
SeqTypes = {
'p': 'P1',
'q': 'P2',
@ -273,23 +269,35 @@ class EasySlidesImport(SongImport):
if tag in versetags:
self.verse_order_list.append(tag)
else:
log.info('Got order item %s, which is not in versetags, dropping item from presentation order',
tag)
log.info('Got order item %s, which is not in versetags, dropping item from presentation order', tag)
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Sequence')
self._success = False
except AttributeError:
pass
def _listHas(self, lst, subitems):
for subitem in subitems:
if subitem in lst:
lst = lst[subitem]
def _list_has(self, lst, sub_items):
"""
See if the list has sub items
:param lst: The list to check
:param sub_items: sub item list
:return:
"""
for sub_item in sub_items:
if sub_item in lst:
lst = lst[sub_item]
else:
return False
return True
def _extractRegion(self, line):
def _extract_region(self, line):
# this was true already: line[0:7] == u'[region':
"""
Extract the region from text
:param line: The line of text
:return:
"""
right_bracket = line.find(']')
return line[7:right_bracket].strip()

View File

@ -75,7 +75,7 @@ class EasyWorshipSongImport(SongImport):
def __init__(self, manager, **kwargs):
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
# Open the DB and MB files if they exist
import_source_mb = self.import_source.replace('.DB', '.MB')
if not os.path.isfile(self.import_source) or not os.path.isfile(import_source_mb):
@ -84,12 +84,12 @@ class EasyWorshipSongImport(SongImport):
if db_size < 0x800:
return
db_file = open(self.import_source, 'rb')
self.memoFile = open(import_source_mb, 'rb')
self.memo_file = open(import_source_mb, 'rb')
# Don't accept files that are clearly not paradox files
record_size, header_size, block_size, first_block, num_fields = struct.unpack('<hhxb8xh17xh', db_file.read(35))
if header_size != 0x800 or block_size < 1 or block_size > 4:
db_file.close()
self.memoFile.close()
self.memo_file.close()
return
# Take a stab at how text is encoded
self.encoding = 'cp1252'
@ -124,20 +124,20 @@ class EasyWorshipSongImport(SongImport):
db_file.seek(4 + (num_fields * 4) + 261, os.SEEK_CUR)
field_names = db_file.read(header_size - db_file.tell()).split(b'\0', num_fields)
field_names.pop()
field_descs = []
field_descriptions = []
for i, field_name in enumerate(field_names):
field_type, field_size = struct.unpack_from('BB', field_info, i * 2)
field_descs.append(FieldDescEntry(field_name, field_type, field_size))
self.setRecordStruct(field_descs)
field_descriptions.append(FieldDescEntry(field_name, field_type, field_size))
self.set_record_struct(field_descriptions)
# Pick out the field description indexes we will need
try:
success = True
fi_title = self.findField(b'Title')
fi_author = self.findField(b'Author')
fi_copy = self.findField(b'Copyright')
fi_admin = self.findField(b'Administrator')
fi_words = self.findField(b'Words')
fi_ccli = self.findField(b'Song Number')
fi_title = self.find_field(b'Title')
fi_author = self.find_field(b'Author')
fi_copy = self.find_field(b'Copyright')
fi_admin = self.find_field(b'Administrator')
fi_words = self.find_field(b'Words')
fi_ccli = self.find_field(b'Song Number')
except IndexError:
# This is the wrong table
success = False
@ -162,15 +162,15 @@ class EasyWorshipSongImport(SongImport):
if self.stop_import_flag:
break
raw_record = db_file.read(record_size)
self.fields = self.recordStruct.unpack(raw_record)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
self.title = self.getField(fi_title).decode()
self.title = self.get_field(fi_title).decode()
# Get remaining fields.
copy = self.getField(fi_copy)
admin = self.getField(fi_admin)
ccli = self.getField(fi_ccli)
authors = self.getField(fi_author)
words = self.getField(fi_words)
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
# Set the SongImport object members.
if copy:
self.copyright = copy.decode()
@ -230,21 +230,21 @@ class EasyWorshipSongImport(SongImport):
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
if len(self.comments) > 5:
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
'\n[above are Song Tags with notes imported from EasyWorship]'))
'\n[above are Song Tags with notes imported from EasyWorship]'))
if self.stop_import_flag:
break
if not self.finish():
self.log_error(self.import_source)
db_file.close()
self.memoFile.close()
self.memo_file.close()
def findField(self, field_name):
return [i for i, x in enumerate(self.fieldDescs) if x.name == field_name][0]
def find_field(self, field_name):
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
def setRecordStruct(self, field_descs):
def set_record_struct(self, field_descriptions):
# Begin with empty field struct list
fsl = ['>']
for field_desc in field_descs:
for field_desc in field_descriptions:
if field_desc.field_type == FieldType.String:
fsl.append('%ds' % field_desc.size)
elif field_desc.field_type == FieldType.Int16:
@ -261,12 +261,18 @@ class EasyWorshipSongImport(SongImport):
fsl.append('Q')
else:
fsl.append('%ds' % field_desc.size)
self.recordStruct = struct.Struct(''.join(fsl))
self.fieldDescs = field_descs
self.record_structure = struct.Struct(''.join(fsl))
self.field_descriptions = field_descriptions
def getField(self, field_desc_index):
def get_field(self, field_desc_index):
"""
Extract the field
:param field_desc_index: Field index value
:return:
"""
field = self.fields[field_desc_index]
field_desc = self.fieldDescs[field_desc_index]
field_desc = self.field_descriptions[field_desc_index]
# Return None in case of 'blank' entries
if isinstance(field, bytes):
if not field.rstrip(b'\0'):
@ -281,23 +287,23 @@ class EasyWorshipSongImport(SongImport):
elif field_desc.field_type == FieldType.Int32:
return field ^ 0x80000000
elif field_desc.field_type == FieldType.Logical:
return (field ^ 0x80 == 1)
return field ^ 0x80 == 1
elif field_desc.field_type == FieldType.Memo or field_desc.field_type == FieldType.Blob:
block_start, blob_size = struct.unpack_from('<II', field, len(field)-10)
sub_block = block_start & 0xff
block_start &= ~0xff
self.memoFile.seek(block_start)
memo_block_type, = struct.unpack('b', self.memoFile.read(1))
self.memo_file.seek(block_start)
memo_block_type, = struct.unpack('b', self.memo_file.read(1))
if memo_block_type == 2:
self.memoFile.seek(8, os.SEEK_CUR)
self.memo_file.seek(8, os.SEEK_CUR)
elif memo_block_type == 3:
if sub_block > 63:
return b''
self.memoFile.seek(11 + (5 * sub_block), os.SEEK_CUR)
sub_block_start, = struct.unpack('B', self.memoFile.read(1))
self.memoFile.seek(block_start + (sub_block_start * 16))
self.memo_file.seek(11 + (5 * sub_block), os.SEEK_CUR)
sub_block_start, = struct.unpack('B', self.memo_file.read(1))
self.memo_file.seek(block_start + (sub_block_start * 16))
else:
return b''
return self.memoFile.read(blob_size)
return self.memo_file.read(blob_size)
else:
return 0

View File

@ -118,7 +118,7 @@ class FoilPresenterImport(SongImport):
SongImport.__init__(self, manager, **kwargs)
self.FoilPresenter = FoilPresenter(self.manager, self)
def doImport(self):
def do_import(self):
"""
Imports the songs.
"""

View File

@ -82,6 +82,7 @@ if os.name == 'nt':
except ImportError:
log.exception('Error importing %s', 'WorshipCenterProImport')
class SongFormatSelect(object):
"""
This is a special enumeration class listing available file selection modes.
@ -99,47 +100,47 @@ class SongFormat(object):
Required attributes for each song format:
``u'class'``
``'class'``
Import class, e.g. ``OpenLyricsImport``
``u'name'``
Name of the format, e.g. ``u'OpenLyrics'``
``'name'``
Name of the format, e.g. ``'OpenLyrics'``
``u'prefix'``
Prefix for Qt objects. Use mixedCase, e.g. ``u'openLyrics'``
``'prefix'``
Prefix for Qt objects. Use mixedCase, e.g. ``'openLyrics'``
See ``SongImportForm.add_file_select_item()``
Optional attributes for each song format:
``u'canDisable'``
``'canDisable'``
Whether song format importer is disablable.
If ``True``, then ``u'disabledLabelText'`` must also be defined.
If ``True``, then ``'disabledLabelText'`` must also be defined.
``u'availability'``
``'availability'``
Whether song format importer is available.
``u'selectMode'``
``'selectMode'``
Whether format accepts single file, multiple files, or single folder
(as per ``SongFormatSelect`` options).
``u'filter'``
``'filter'``
File extension filter for ``QFileDialog``.
Optional/custom text Strings for ``SongImportForm`` widgets:
``u'comboBoxText'``
Combo box selector (default value is the format's ``u'name'``).
``'comboBoxText'``
Combo box selector (default value is the format's ``'name'``).
``u'disabledLabelText'``
``'disabledLabelText'``
Required for disablable song formats.
``u'getFilesTitle'``
Title for ``QFileDialog`` (default includes the format's ``u'name'``).
``'getFilesTitle'``
Title for ``QFileDialog`` (default includes the format's ``'name'``).
``u'invalidSourceMsg'``
``'invalidSourceMsg'``
Message displayed if ``is_valid_source()`` returns ``False``.
``u'descriptionText'``
``'descriptionText'``
Short description (1-2 lines) about the song format.
"""
# Song formats (ordered alphabetically after Generic)
@ -200,8 +201,8 @@ class SongFormat(object):
'prefix': 'generic',
'canDisable': True,
'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
'The generic document/presentation importer has been disabled '
'because OpenLP cannot access OpenOffice or LibreOffice.'),
'The generic document/presentation importer has been disabled '
'because OpenLP cannot access OpenOffice or LibreOffice.'),
'getFilesTitle': translate('SongsPlugin.ImportWizardForm', 'Select Document/Presentation Files')
},
CCLI: {
@ -241,13 +242,12 @@ class SongFormat(object):
'prefix': 'mediaShout',
'canDisable': True,
'selectMode': SongFormatSelect.SingleFile,
'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm',
'MediaShout Database'),
'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'MediaShout Database'),
'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
'The MediaShout importer is only supported on Windows. It has '
'been disabled due to a missing Python module. If you want to '
'use this importer, you will need to install the "pyodbc" '
'module.')
'The MediaShout importer is only supported on Windows. It has '
'been disabled due to a missing Python module. If you want to '
'use this importer, you will need to install the "pyodbc" '
'module.')
},
OpenSong: {
'class': OpenSongImport,
@ -259,15 +259,14 @@ class SongFormat(object):
'name': 'PowerSong 1.0',
'prefix': 'powerSong',
'selectMode': SongFormatSelect.SingleFolder,
'invalidSourceMsg': translate('SongsPlugin.ImportWizardForm',
'You need to specify a valid PowerSong 1.0 database folder.')
'invalidSourceMsg': translate('SongsPlugin.ImportWizardForm', 'You need to specify a valid PowerSong 1.0 '
'database folder.')
},
SongBeamer: {
'class': SongBeamerImport,
'name': 'SongBeamer',
'prefix': 'songBeamer',
'filter': '%s (*.sng)' % translate('SongsPlugin.ImportWizardForm',
'SongBeamer Files')
'filter': '%s (*.sng)' % translate('SongsPlugin.ImportWizardForm', 'SongBeamer Files')
},
SongPro: {
'class': SongProImport,
@ -277,7 +276,7 @@ class SongFormat(object):
'filter': '%s (*.txt)' % translate('SongsPlugin.ImportWizardForm', 'SongPro Text Files'),
'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'SongPro (Export File)'),
'descriptionText': translate('SongsPlugin.ImportWizardForm',
'In SongPro, export your songs using the File -> Export menu')
'In SongPro, export your songs using the File -> Export menu')
},
SongShowPlus: {
'class': SongShowPlusImport,
@ -291,8 +290,8 @@ class SongFormat(object):
'canDisable': True,
'filter': '%s (*.rtf)' % translate('SongsPlugin.ImportWizardForm', 'Songs Of Fellowship Song Files'),
'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
'The Songs of Fellowship importer has been disabled because '
'OpenLP cannot access OpenOffice or LibreOffice.')
'The Songs of Fellowship importer has been disabled because '
'OpenLP cannot access OpenOffice or LibreOffice.')
},
SundayPlus: {
'class': SundayPlusImport,
@ -304,8 +303,7 @@ class SongFormat(object):
'class': WowImport,
'name': 'Words of Worship',
'prefix': 'wordsOfWorship',
'filter': '%s (*.wsg *.wow-song)' %
translate('SongsPlugin.ImportWizardForm', 'Words Of Worship Song Files')
'filter': '%s (*.wsg *.wow-song)' % translate('SongsPlugin.ImportWizardForm', 'Words Of Worship Song Files')
},
WorshipCenterPro: {
'name': 'WorshipCenter Pro',
@ -314,8 +312,9 @@ class SongFormat(object):
'selectMode': SongFormatSelect.SingleFile,
'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'WorshipCenter Pro Song Files'),
'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
'The WorshipCenter Pro importer is only supported on Windows. It has been disabled due to a missing '
'Python module. If you want to use this importer, you will need to install the "pyodbc" module.')
'The WorshipCenter Pro importer is only supported on Windows. It has been '
'disabled due to a missing Python module. If you want to use this '
'importer, you will need to install the "pyodbc" module.')
},
ZionWorx: {
'class': ZionWorxImport,
@ -324,9 +323,9 @@ class SongFormat(object):
'selectMode': SongFormatSelect.SingleFile,
'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'ZionWorx (CSV)'),
'descriptionText': translate('SongsPlugin.ImportWizardForm',
'First convert your ZionWorx database to a CSV text file, as '
'explained in the <a href="http://manual.openlp.org/songs.html'
'#importing-from-zionworx">User Manual</a>.')
'First convert your ZionWorx database to a CSV text file, as '
'explained in the <a href="http://manual.openlp.org/songs.html'
'#importing-from-zionworx">User Manual</a>.')
}
}

View File

@ -36,8 +36,8 @@ from PyQt4 import QtCore, QtGui
from sqlalchemy.sql import or_
from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \
check_item_selected, create_separated_list
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, check_item_selected, \
create_separated_list
from openlp.core.lib.ui import create_widget_action
from openlp.plugins.songs.forms.editsongform import EditSongForm
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm

View File

@ -48,7 +48,7 @@ class MediaShoutImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file to import.
"""

View File

@ -63,7 +63,7 @@ class OpenLPSongImport(SongImport):
SongImport.__init__(self, manager, **kwargs)
self.sourceSession = None
def doImport(self, progressDialog=None):
def do_import(self, progressDialog=None):
"""
Run the import for an OpenLP version 2 song database.

View File

@ -58,18 +58,17 @@ class OooImport(SongImport):
"""
def __init__(self, manager, **kwargs):
"""
Initialise the class. Requires a songmanager class which is passed
to SongImport for writing song to disk
Initialise the class. Requires a songmanager class which is passed to SongImport for writing song to disk
"""
SongImport.__init__(self, manager, **kwargs)
self.document = None
self.processStarted = False
self.process_started = False
def doImport(self):
def do_import(self):
if not isinstance(self.import_source, list):
return
try:
self.startOoo()
self.start_ooo()
except NoConnectException as exc:
self.log_error(
self.import_source[0],
@ -82,34 +81,34 @@ class OooImport(SongImport):
break
filename = str(filename)
if os.path.isfile(filename):
self.openOooFile(filename)
self.open_ooo_file(filename)
if self.document:
self.processOooDocument()
self.closeOooFile()
self.process_ooo_document()
self.close_ooo_file()
else:
self.log_error(self.filepath, translate('SongsPlugin.SongImport', 'Unable to open file'))
self.log_error(self.file_path, translate('SongsPlugin.SongImport', 'Unable to open file'))
else:
self.log_error(self.filepath, translate('SongsPlugin.SongImport', 'File not found'))
self.closeOoo()
self.log_error(self.file_path, translate('SongsPlugin.SongImport', 'File not found'))
self.close_ooo()
def processOooDocument(self):
def process_ooo_document(self):
"""
Handle the import process for OpenOffice files. This method facilitates
allowing subclasses to handle specific types of OpenOffice files.
"""
if self.document.supportsService("com.sun.star.presentation.PresentationDocument"):
self.processPres()
self.process_presentation()
if self.document.supportsService("com.sun.star.text.TextDocument"):
self.processDoc()
self.process_doc()
def startOoo(self):
def start_ooo(self):
"""
Start OpenOffice.org process
TODO: The presentation/Impress plugin may already have it running
"""
if os.name == 'nt':
self.startOooProcess()
self.desktop = self.oooManager.createInstance('com.sun.star.frame.Desktop')
self.start_ooo_process()
self.desktop = self.ooo_manager.createInstance('com.sun.star.frame.Desktop')
else:
context = uno.getComponentContext()
resolver = context.ServiceManager.createInstanceWithContext('com.sun.star.bridge.UnoUrlResolver', context)
@ -121,7 +120,7 @@ class OooImport(SongImport):
except NoConnectException:
time.sleep(0.1)
log.exception("Failed to resolve uno connection")
self.startOooProcess()
self.start_ooo_process()
loop += 1
else:
manager = uno_instance.ServiceManager
@ -129,60 +128,62 @@ class OooImport(SongImport):
return
raise Exception('Unable to start LibreOffice')
def startOooProcess(self):
def start_ooo_process(self):
"""
Start the OO Process
"""
try:
if os.name == 'nt':
self.oooManager = Dispatch('com.sun.star.ServiceManager')
self.oooManager._FlagAsMethod('Bridge_GetStruct')
self.oooManager._FlagAsMethod('Bridge_GetValueObject')
self.ooo_manager = Dispatch('com.sun.star.ServiceManager')
self.ooo_manager._FlagAsMethod('Bridge_GetStruct')
self.ooo_manager._FlagAsMethod('Bridge_GetValueObject')
else:
cmd = get_uno_command()
process = QtCore.QProcess()
process.startDetached(cmd)
self.processStarted = True
self.process_started = True
except:
log.exception("startOooProcess failed")
log.exception("start_ooo_process failed")
def openOooFile(self, filepath):
def open_ooo_file(self, file_path):
"""
Open the passed file in OpenOffice.org Impress
"""
self.filepath = filepath
self.file_path = file_path
if os.name == 'nt':
url = filepath.replace('\\', '/')
url = file_path.replace('\\', '/')
url = url.replace(':', '|').replace(' ', '%20')
url = 'file:///' + url
else:
url = uno.systemPathToFileUrl(filepath)
url = uno.systemPathToFileUrl(file_path)
properties = []
properties = tuple(properties)
try:
self.document = self.desktop.loadComponentFromURL(url, '_blank',
0, properties)
self.document = self.desktop.loadComponentFromURL(url, '_blank', 0, properties)
if not self.document.supportsService("com.sun.star.presentation.PresentationDocument") and not \
self.document.supportsService("com.sun.star.text.TextDocument"):
self.closeOooFile()
self.close_ooo_file()
else:
self.import_wizard.increment_progress_bar('Processing file ' + filepath, 0)
self.import_wizard.increment_progress_bar('Processing file ' + file_path, 0)
except AttributeError:
log.exception("openOooFile failed: %s", url)
log.exception("open_ooo_file failed: %s", url)
return
def closeOooFile(self):
def close_ooo_file(self):
"""
Close file.
"""
self.document.close(True)
self.document = None
def closeOoo(self):
def close_ooo(self):
"""
Close OOo. But only if we started it and not on windows
"""
if self.processStarted:
if self.process_started:
self.desktop.terminate()
def processPres(self):
def process_presentation(self):
"""
Process the file
"""
@ -194,45 +195,50 @@ class OooImport(SongImport):
self.import_wizard.increment_progress_bar('Import cancelled', 0)
return
slide = slides.getByIndex(slide_no)
slidetext = ''
slide_text = ''
for idx in range(slide.getCount()):
shape = slide.getByIndex(idx)
if shape.supportsService("com.sun.star.drawing.Text"):
if shape.getString().strip() != '':
slidetext += shape.getString().strip() + '\n\n'
if slidetext.strip() == '':
slidetext = '\f'
text += slidetext
self.processSongsText(text)
slide_text += shape.getString().strip() + '\n\n'
if slide_text.strip() == '':
slide_text = '\f'
text += slide_text
self.process_songs_text(text)
return
def processDoc(self):
def process_doc(self):
"""
Process the doc file, a paragraph at a time
"""
text = ''
paragraphs = self.document.getText().createEnumeration()
while paragraphs.hasMoreElements():
paratext = ''
para_text = ''
paragraph = paragraphs.nextElement()
if paragraph.supportsService("com.sun.star.text.Paragraph"):
textportions = paragraph.createEnumeration()
while textportions.hasMoreElements():
textportion = textportions.nextElement()
if textportion.BreakType in (PAGE_BEFORE, PAGE_BOTH):
paratext += '\f'
paratext += textportion.getString()
if textportion.BreakType in (PAGE_AFTER, PAGE_BOTH):
paratext += '\f'
text += paratext + '\n'
self.processSongsText(text)
text_portions = paragraph.createEnumeration()
while text_portions.hasMoreElements():
text_portion = text_portions.nextElement()
if text_portion.BreakType in (PAGE_BEFORE, PAGE_BOTH):
para_text += '\f'
para_text += text_portion.getString()
if text_portion.BreakType in (PAGE_AFTER, PAGE_BOTH):
para_text += '\f'
text += para_text + '\n'
self.process_songs_text(text)
def processSongsText(self, text):
songtexts = self.tidy_text(text).split('\f')
def process_songs_text(self, text):
"""
Process the songs text
:param text: The text.
"""
song_texts = self.tidy_text(text).split('\f')
self.set_defaults()
for songtext in songtexts:
if songtext.strip():
self.process_song_text(songtext.strip())
for song_text in song_texts:
if song_text.strip():
self.process_song_text(song_text.strip())
if self.check_complete():
self.finish()
self.set_defaults()

View File

@ -56,7 +56,7 @@ class OpenLyricsImport(SongImport):
SongImport.__init__(self, manager, **kwargs)
self.openLyrics = OpenLyrics(self.manager)
def doImport(self):
def do_import(self):
"""
Imports the songs.
"""

View File

@ -109,7 +109,7 @@ class OpenSongImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
for filename in self.import_source:
if self.stop_import_flag:

View File

@ -85,7 +85,7 @@ class PowerSongImport(SongImport):
return True
return False
def doImport(self):
def do_import(self):
"""
Receive either a list of files or a folder (unicode) to import.
"""

View File

@ -61,6 +61,7 @@ try:
except ImportError:
ITALIC = 2
class SofImport(OooImport):
"""
Import songs provided on disks with the Songs of Fellowship music books
@ -85,7 +86,7 @@ class SofImport(OooImport):
OooImport.__init__(self, manager, **kwargs)
self.song = False
def processOooDocument(self):
def process_ooo_document(self):
"""
Handle the import process for SoF files.
"""
@ -108,7 +109,7 @@ class SofImport(OooImport):
except RuntimeException as exc:
log.exception('Error processing file: %s', exc)
if not self.finish():
self.log_error(self.filepath)
self.log_error(self.file_path)
def processParagraph(self, paragraph):
"""

View File

@ -98,7 +98,7 @@ class SongBeamerImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file or a list of files to import.
"""

View File

@ -154,12 +154,22 @@ class SongImport(QtCore.QObject):
return text
def process_song_text(self, text):
"""
Process the song text from import
:param text: Some text
"""
verse_texts = text.split('\n\n')
for verse_text in verse_texts:
if verse_text.strip() != '':
self.process_verse_text(verse_text.strip())
def process_verse_text(self, text):
"""
Process the song verse text from import
:param text: Some text
"""
lines = text.split('\n')
if text.lower().find(self.copyright_string) >= 0 or text.find(str(SongStrings.CopyrightSymbol)) >= 0:
copyright_found = False
@ -190,9 +200,8 @@ class SongImport(QtCore.QObject):
def parse_author(self, text):
"""
Add the author. OpenLP stores them individually so split by 'and', '&'
and comma. However need to check for 'Mr and Mrs Smith' and turn it to
'Mr Smith' and 'Mrs Smith'.
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
"""
for author in text.split(','):
authors = author.split('&')
@ -227,16 +236,10 @@ class SongImport(QtCore.QObject):
attempt to detect duplicates. In this case it will just add to the verse
order.
``verse_text``
The text of the verse.
``verse_def``
The verse tag can be v1/c1/b etc, or 'v' and 'c' (will count the
:param verse_text: The text of the verse.
:param verse_def: The verse tag can be v1/c1/b etc, or 'v' and 'c' (will count the
verses/choruses itself) or None, where it will assume verse.
``lang``
The language code (ISO-639) of the verse, for example *en* or *de*.
:param lang: The language code (ISO-639) of the verse, for example *en* or *de*.
"""
for (old_verse_def, old_verse, old_lang) in self.verses:
if old_verse.strip() == verse_text.strip():
@ -317,24 +320,24 @@ class SongImport(QtCore.QObject):
song.comments = self.comments
song.theme_name = self.theme_name
song.ccli_number = self.ccli_number
for authortext in self.authors:
author = self.manager.get_object_filtered(Author, Author.display_name == authortext)
for author_text in self.authors:
author = self.manager.get_object_filtered(Author, Author.display_name == author_text)
if not author:
author = Author.populate(display_name=authortext,
last_name=authortext.split(' ')[-1],
first_name=' '.join(authortext.split(' ')[:-1]))
author = Author.populate(display_name=author_text,
last_name=author_text.split(' ')[-1],
first_name=' '.join(author_text.split(' ')[:-1]))
song.authors.append(author)
if self.song_book_name:
song_book = self.manager.get_object_filtered(Book, Book.name == self.song_book_name)
if song_book is None:
song_book = Book.populate(name=self.song_book_name, publisher=self.song_book_pub)
song.book = song_book
for topictext in self.topics:
if not topictext:
for topic_text in self.topics:
if not topic_text:
continue
topic = self.manager.get_object_filtered(Topic, Topic.name == topictext)
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None:
topic = Topic.populate(name=topictext)
topic = Topic.populate(name=topic_text)
song.topics.append(topic)
# We need to save the song now, before adding the media files, so that
# we know where to save the media files to.
@ -357,14 +360,14 @@ class SongImport(QtCore.QObject):
This method copies the media file to the correct location and returns
the new file location.
``filename``
The file to copy.
:param song_id:
:param filename: The file to copy.
"""
if not hasattr(self, 'save_path'):
self.save_path = os.path.join(AppLocation.get_section_data_path(self.import_wizard.plugin.name),
'audio', str(song_id))
'audio', str(song_id))
check_directory_exists(self.save_path)
if not filename.startswith(self.save_path):
oldfile, filename = filename, os.path.join(self.save_path, os.path.split(filename)[1])
shutil.copyfile(oldfile, filename)
old_file, filename = filename, os.path.join(self.save_path, os.path.split(filename)[1])
shutil.copyfile(old_file, filename)
return filename

View File

@ -74,7 +74,7 @@ class SongProImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file or a list of files to import.
"""

View File

@ -94,7 +94,7 @@ class SongShowPlusImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file or a list of files to import.
"""

View File

@ -62,7 +62,7 @@ class SundayPlusImport(SongImport):
SongImport.__init__(self, manager, **kwargs)
self.encoding = 'us-ascii'
def doImport(self):
def do_import(self):
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
for filename in self.import_source:
if self.stop_import_flag:

View File

@ -51,7 +51,7 @@ class WorshipCenterProImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file to import.
"""

View File

@ -101,7 +101,7 @@ class WowImport(SongImport):
"""
SongImport.__init__(self, manager, **kwargs)
def doImport(self):
def do_import(self):
"""
Receive a single file or a list of files to import.
"""

View File

@ -78,7 +78,7 @@ class ZionWorxImport(SongImport):
* Note: This is the default format of the Python ``csv`` module.
"""
def doImport(self):
def do_import(self):
"""
Receive a CSV file (from a ZionWorx database dump) to import.
"""

View File

@ -289,7 +289,7 @@ class SongsPlugin(Plugin):
self.application.process_events()
for db in song_dbs:
importer = OpenLPSongImport(self.manager, filename=db)
importer.doImport(progress)
importer.do_import(progress)
self.application.process_events()
progress.setValue(song_count)
self.media_item.on_search_text_button_clicked()

View File

@ -154,13 +154,13 @@ class TestEasyWorshipSongImport(TestCase):
def find_field_exists_test(self):
"""
Test finding an existing field in a given list using the :mod:`findField`
Test finding an existing field in a given list using the :mod:`find_field`
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions.
with patch('openlp.plugins.songs.lib.ewimport.SongImport'):
mocked_manager = MagicMock()
importer = EasyWorshipSongImport(mocked_manager)
importer.fieldDescs = TEST_FIELD_DESCS
importer.field_descriptions = TEST_FIELD_DESCS
# WHEN: Called with a field name that exists
existing_fields = ['Title', 'Text Percentage Bottom', 'RecID', 'Default Background', 'Words',
@ -168,28 +168,28 @@ class TestEasyWorshipSongImport(TestCase):
for field_name in existing_fields:
# THEN: The item corresponding the index returned should have the same name attribute
self.assertEquals(importer.fieldDescs[importer.findField(field_name)].name, field_name)
self.assertEquals(importer.field_descriptions[importer.find_field(field_name)].name, field_name)
def find_non_existing_field_test(self):
"""
Test finding an non-existing field in a given list using the :mod:`findField`
Test finding an non-existing field in a given list using the :mod:`find_field`
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions
with patch('openlp.plugins.songs.lib.ewimport.SongImport'):
mocked_manager = MagicMock()
importer = EasyWorshipSongImport(mocked_manager)
importer.fieldDescs = TEST_FIELD_DESCS
importer.field_descriptions = TEST_FIELD_DESCS
# WHEN: Called with a field name that does not exist
non_existing_fields = ['BK Gradient Shading', 'BK Gradient Variant', 'Favorite', 'Copyright']
for field_name in non_existing_fields:
# THEN: The importer object should not be None
self.assertRaises(IndexError, importer.findField, field_name)
self.assertRaises(IndexError, importer.find_field, field_name)
def set_record_struct_test(self):
"""
Test the :mod:`setRecordStruct` module
Test the :mod:`set_record_struct` module
"""
# GIVEN: A mocked out SongImport class, a mocked out struct class, and a mocked out "manager" and a list of
# field descriptions
@ -198,17 +198,17 @@ class TestEasyWorshipSongImport(TestCase):
mocked_manager = MagicMock()
importer = EasyWorshipSongImport(mocked_manager)
# WHEN: setRecordStruct is called with a list of field descriptions
return_value = importer.setRecordStruct(TEST_FIELD_DESCS)
# WHEN: set_record_struct is called with a list of field descriptions
return_value = importer.set_record_struct(TEST_FIELD_DESCS)
# THEN: setRecordStruct should return None and Struct should be called with a value representing
# THEN: set_record_struct should return None and Struct should be called with a value representing
# the list of field descriptions
self.assertIsNone(return_value, 'setRecordStruct should return None')
self.assertIsNone(return_value, 'set_record_struct should return None')
mocked_struct.Struct.assert_called_with('>50sHIB250s250s10sQ')
def get_field_test(self):
"""
Test the :mod:`getField` module
Test the :mod:`get_field` module
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager", an encoding and some test data and known results
with patch('openlp.plugins.songs.lib.ewimport.SongImport'):
@ -216,20 +216,20 @@ class TestEasyWorshipSongImport(TestCase):
importer = EasyWorshipSongImport(mocked_manager)
importer.encoding = TEST_DATA_ENCODING
importer.fields = TEST_FIELDS
importer.fieldDescs = TEST_FIELD_DESCS
importer.field_descriptions = TEST_FIELD_DESCS
field_results = [(0, b'A Heart Like Thine'), (1, 100), (2, 102), (3, True), (6, None), (7, None)]
# WHEN: Called with test data
for field_index, result in field_results:
return_value = importer.getField(field_index)
return_value = importer.get_field(field_index)
# THEN: getField should return the known results
# THEN: get_field should return the known results
self.assertEquals(return_value, result,
'getField should return "%s" when called with "%s"' % (result, TEST_FIELDS[field_index]))
'get_field should return "%s" when called with "%s"' % (result, TEST_FIELDS[field_index]))
def get_memo_field_test(self):
"""
Test the :mod:`getField` module
Test the :mod:`get_field` module
"""
for test_results in GET_MEMO_FIELD_TEST_RESULTS:
# GIVEN: A mocked out SongImport class, a mocked out "manager", a mocked out memo_file and an encoding
@ -237,20 +237,20 @@ class TestEasyWorshipSongImport(TestCase):
mocked_manager = MagicMock()
mocked_memo_file = MagicMock()
importer = EasyWorshipSongImport(mocked_manager)
importer.memoFile = mocked_memo_file
importer.memo_file = mocked_memo_file
importer.encoding = TEST_DATA_ENCODING
# WHEN: Supplied with test fields and test field descriptions
importer.fields = TEST_FIELDS
importer.fieldDescs = TEST_FIELD_DESCS
importer.field_descriptions = TEST_FIELD_DESCS
field_index = test_results[0]
mocked_memo_file.read.return_value = test_results[1]
get_field_result = test_results[2]['return']
get_field_read_calls = test_results[2]['read']
get_field_seek_calls = test_results[2]['seek']
# THEN: getField should return the appropriate value with the appropriate mocked objects being called
self.assertEquals(importer.getField(field_index), get_field_result)
# THEN: get_field should return the appropriate value with the appropriate mocked objects being called
self.assertEquals(importer.get_field(field_index), get_field_result)
for call in get_field_read_calls:
mocked_memo_file.read.assert_any_call(call)
for call in get_field_seek_calls:
@ -261,7 +261,7 @@ class TestEasyWorshipSongImport(TestCase):
def do_import_source_test(self):
"""
Test the :mod:`doImport` module opens the correct files
Test the :mod:`do_import` module opens the correct files
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager"
with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
@ -273,14 +273,14 @@ class TestEasyWorshipSongImport(TestCase):
# WHEN: Supplied with an import source
importer.import_source = 'Songs.DB'
# THEN: doImport should return None having called os.path.isfile
self.assertIsNone(importer.doImport(), 'doImport should return None')
# THEN: do_import should return None having called os.path.isfile
self.assertIsNone(importer.do_import(), 'do_import should return None')
mocked_os_path.isfile.assert_any_call('Songs.DB')
mocked_os_path.isfile.assert_any_call('Songs.MB')
def do_import_database_validity_test(self):
"""
Test the :mod:`doImport` module handles invalid database files correctly
Test the :mod:`do_import` module handles invalid database files correctly
"""
# GIVEN: A mocked out SongImport class, os.path and a mocked out "manager"
with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
@ -293,13 +293,13 @@ class TestEasyWorshipSongImport(TestCase):
# WHEN: DB file size is less than 0x800
mocked_os_path.getsize.return_value = 0x7FF
# THEN: doImport should return None having called os.path.isfile
self.assertIsNone(importer.doImport(), 'doImport should return None when db_size is less than 0x800')
# THEN: do_import should return None having called os.path.isfile
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
mocked_os_path.getsize.assert_any_call('Songs.DB')
def do_import_memo_validty_test(self):
"""
Test the :mod:`doImport` module handles invalid memo files correctly
Test the :mod:`do_import` module handles invalid memo files correctly
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager"
with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
@ -316,9 +316,9 @@ class TestEasyWorshipSongImport(TestCase):
struct_unpack_return_values = [(0, 0x700, 2, 0, 0), (0, 0x800, 0, 0, 0), (0, 0x800, 5, 0, 0)]
mocked_struct.unpack.side_effect = struct_unpack_return_values
# THEN: doImport should return None having called closed the open files db and memo files.
# THEN: do_import should return None having called closed the open files db and memo files.
for effect in struct_unpack_return_values:
self.assertIsNone(importer.doImport(), 'doImport should return None when db_size is less than 0x800')
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
self.assertEqual(mocked_open().close.call_count, 2,
'The open db and memo files should have been closed')
mocked_open().close.reset_mock()
@ -326,7 +326,7 @@ class TestEasyWorshipSongImport(TestCase):
def code_page_to_encoding_test(self):
"""
Test the :mod:`doImport` converts the code page to the encoding correctly
Test the :mod:`do_import` converts the code page to the encoding correctly
"""
# GIVEN: A mocked out SongImport class, a mocked out "manager"
with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
@ -345,8 +345,8 @@ class TestEasyWorshipSongImport(TestCase):
mocked_struct.unpack.side_effect = struct_unpack_return_values
mocked_retrieve_windows_encoding.return_value = False
# THEN: doImport should return None having called retrieve_windows_encoding with the correct encoding.
self.assertIsNone(importer.doImport(), 'doImport should return None when db_size is less than 0x800')
# THEN: do_import should return None having called retrieve_windows_encoding with the correct encoding.
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
mocked_retrieve_windows_encoding.assert_call(encoding)
def file_import_test(self):
@ -378,9 +378,9 @@ class TestEasyWorshipSongImport(TestCase):
# WHEN: Importing each file
importer.import_source = os.path.join(TEST_PATH, 'Songs.DB')
# THEN: doImport should return none, the song data should be as expected, and finish should have been
# THEN: do_import should return none, the song data should be as expected, and finish should have been
# called.
self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed')
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
for song_data in SONG_TEST_DATA:
title = song_data['title']
author_calls = song_data['authors']

View File

@ -73,7 +73,7 @@ class TestSongBeamerImport(TestCase):
def invalid_import_source_test(self):
"""
Test SongBeamerImport.doImport handles different invalid import_source values
Test SongBeamerImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'):
@ -87,14 +87,14 @@ class TestSongBeamerImport(TestCase):
for source in ['not a list', 0]:
importer.import_source = source
# THEN: doImport should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.doImport(), 'doImport should return None when import_source is not a list')
# THEN: do_import should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
'setMaxium on import_wizard.progress_bar should not have been called')
def valid_import_source_test(self):
"""
Test SongBeamerImport.doImport handles different invalid import_source values
Test SongBeamerImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'):
@ -107,10 +107,10 @@ class TestSongBeamerImport(TestCase):
# WHEN: Import source is a list
importer.import_source = ['List', 'of', 'files']
# THEN: doImport should return none and the progress bar setMaximum should be called with the length of
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
# import_source.
self.assertIsNone(importer.doImport(),
'doImport should return None when import_source is a list and stop_import_flag is True')
self.assertIsNone(importer.do_import(),
'do_import should return None when import_source is a list and stop_import_flag is True')
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
def file_import_test(self):
@ -140,9 +140,9 @@ class TestSongBeamerImport(TestCase):
song_book_name = SONG_TEST_DATA[song_file]['song_book_name']
song_number = SONG_TEST_DATA[song_file]['song_number']
# THEN: doImport should return none, the song data should be as expected, and finish should have been
# THEN: do_import should return none, the song data should be as expected, and finish should have been
# called.
self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed')
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (song_file, title))
for verse_text, verse_tag in add_verse_calls:
mocked_add_verse.assert_any_call(verse_text, verse_tag)

View File

@ -43,6 +43,7 @@ TEST_PATH = os.path.abspath(
class TestSongShowPlusFileImport(SongImportTestHelper):
def __init__(self, *args, **kwargs):
self.importer_class_name = 'SongShowPlusImport'
self.importer_module_name = 'songshowplusimport'
@ -75,7 +76,7 @@ class TestSongShowPlusImport(TestCase):
def invalid_import_source_test(self):
"""
Test SongShowPlusImport.doImport handles different invalid import_source values
Test SongShowPlusImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.songshowplusimport.SongImport'):
@ -89,14 +90,14 @@ class TestSongShowPlusImport(TestCase):
for source in ['not a list', 0]:
importer.import_source = source
# THEN: doImport should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.doImport(), 'doImport should return None when import_source is not a list')
# THEN: do_import should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
'setMaximum on import_wizard.progress_bar should not have been called')
def valid_import_source_test(self):
"""
Test SongShowPlusImport.doImport handles different invalid import_source values
Test SongShowPlusImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.songshowplusimport.SongImport'):
@ -109,10 +110,10 @@ class TestSongShowPlusImport(TestCase):
# WHEN: Import source is a list
importer.import_source = ['List', 'of', 'files']
# THEN: doImport should return none and the progress bar setMaximum should be called with the length of
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
# import_source.
self.assertIsNone(importer.doImport(),
'doImport should return None when import_source is a list and stop_import_flag is True')
self.assertIsNone(importer.do_import(),
'do_import should return None when import_source is a list and stop_import_flag is True')
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
def to_openlp_verse_tag_test(self):

View File

@ -165,12 +165,12 @@ class TestWorshipCenterProSongImport(TestCase):
pyodbc_errors = [pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError]
mocked_pyodbc_connect.side_effect = pyodbc_errors
# WHEN: Calling the doImport method
# WHEN: Calling the do_import method
for effect in pyodbc_errors:
return_value = importer.doImport()
return_value = importer.do_import()
# THEN: doImport should return None, and pyodbc, translate & log_error are called with known calls
self.assertIsNone(return_value, 'doImport should return None when pyodbc raises an exception.')
# THEN: do_import should return None, and pyodbc, translate & log_error are called with known calls
self.assertIsNone(return_value, 'do_import should return None when pyodbc raises an exception.')
mocked_pyodbc_connect.assert_called_with( 'DRIVER={Microsoft Access Driver (*.mdb)};DBQ=import_source')
mocked_translate.assert_called_with('SongsPlugin.WorshipCenterProImport',
'Unable to connect the WorshipCenter Pro database.')
@ -198,13 +198,13 @@ class TestWorshipCenterProSongImport(TestCase):
importer.stop_import_flag = False
importer.finish = mocked_finish
# WHEN: Calling the doImport method
return_value = importer.doImport()
# WHEN: Calling the do_import method
return_value = importer.do_import()
# THEN: doImport should return None, and pyodbc, import_wizard, importer.title and add_verse are called with
# THEN: do_import should return None, and pyodbc, import_wizard, importer.title and add_verse are called with
# known calls
self.assertIsNone(return_value, 'doImport should return None when pyodbc raises an exception.')
self.assertIsNone(return_value, 'do_import should return None when pyodbc raises an exception.')
mocked_pyodbc.connect.assert_called_with('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=import_source')
mocked_pyodbc.connect().cursor.assert_any_call()
mocked_pyodbc.connect().cursor().execute.assert_called_with('SELECT ID, Field, Value FROM __SONGDATA')

View File

@ -107,9 +107,9 @@ class SongImportTestHelper(TestCase):
topics = self._get_data(result_data, 'topics')
verse_order_list = self._get_data(result_data, 'verse_order_list')
# THEN: doImport should return none, the song data should be as expected, and finish should have been
# THEN: do_import should return none, the song data should be as expected, and finish should have been
# called.
self.assertIsNone(importer.doImport(), 'doImport should return None when it has completed')
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title))
for author in author_calls:
self.mocked_parse_author.assert_any_call(author)