This commit is contained in:
Andreas Preikschat 2012-05-07 17:45:02 +02:00
commit 43892f7d53
11 changed files with 396 additions and 92 deletions

View File

@ -46,13 +46,36 @@ class FormattingTags(object):
"""
Provide access to the html_expands list.
"""
# Load user defined tags otherwise user defined tags are not present.
return FormattingTags.html_expands
@staticmethod
def reset_html_tags():
def save_html_tags():
"""
Resets the html_expands list.
Saves all formatting tags except protected ones.
"""
tags = []
for tag in FormattingTags.html_expands:
if not tag[u'protected'] and not tag.get(u'temporary'):
# Using dict ensures that copy is made and encoding of values
# a little later does not affect tags in the original list
tags.append(dict(tag))
tag = tags[-1]
# Remove key 'temporary' from tags.
# It is not needed to be saved.
if u'temporary' in tag:
del tag[u'temporary']
for element in tag:
if isinstance(tag[element], unicode):
tag[element] = tag[element].encode('utf8')
# Formatting Tags were also known as display tags.
QtCore.QSettings().setValue(u'displayTags/html_tags',
QtCore.QVariant(cPickle.dumps(tags) if tags else u''))
@staticmethod
def load_tags():
"""
Load the Tags from store so can be used in the system or used to
update the display.
"""
temporary_tags = [tag for tag in FormattingTags.html_expands
if tag.get(u'temporary')]
@ -140,38 +163,6 @@ class FormattingTags(object):
FormattingTags.add_html_tags(base_tags)
FormattingTags.add_html_tags(temporary_tags)
@staticmethod
def save_html_tags():
"""
Saves all formatting tags except protected ones.
"""
tags = []
for tag in FormattingTags.html_expands:
if not tag[u'protected'] and not tag.get(u'temporary'):
# Using dict ensures that copy is made and encoding of values
# a little later does not affect tags in the original list
tags.append(dict(tag))
tag = tags[-1]
# Remove key 'temporary' from tags.
# It is not needed to be saved.
if u'temporary' in tag:
del tag[u'temporary']
for element in tag:
if isinstance(tag[element], unicode):
tag[element] = tag[element].encode('utf8')
# Formatting Tags were also known as display tags.
QtCore.QSettings().setValue(u'displayTags/html_tags',
QtCore.QVariant(cPickle.dumps(tags) if tags else u''))
@staticmethod
def load_tags():
"""
Load the Tags from store so can be used in the system or used to
update the display. If Cancel was selected this is needed to reset the
dsiplay to the correct version.
"""
# Initial Load of the Tags
FormattingTags.reset_html_tags()
# Formatting Tags were also known as display tags.
user_expands = QtCore.QSettings().value(u'displayTags/html_tags',
QtCore.QVariant(u'')).toString()
@ -187,17 +178,13 @@ class FormattingTags(object):
FormattingTags.add_html_tags(user_tags)
@staticmethod
def add_html_tags(tags, save=False):
def add_html_tags(tags):
"""
Add a list of tags to the list.
``tags``
The list with tags to add.
``save``
Defaults to ``False``. If set to ``True`` the given ``tags`` are
saved to the config.
Each **tag** has to be a ``dict`` and should have the following keys:
* desc
@ -225,8 +212,6 @@ class FormattingTags(object):
displaying text containing the tag. It has to be a ``boolean``.
"""
FormattingTags.html_expands.extend(tags)
if save:
FormattingTags.save_html_tags()
@staticmethod
def remove_html_tag(tag_id):

View File

@ -100,6 +100,7 @@ class Image(object):
variables ``image`` and ``image_bytes`` to ``None`` and add the image object
to the queue of images to process.
"""
secondary_priority = 0
def __init__(self, name, path, source, background):
self.name = name
self.path = path
@ -108,25 +109,40 @@ class Image(object):
self.priority = Priority.Normal
self.source = source
self.background = background
self.secondary_priority = Image.secondary_priority
Image.secondary_priority += 1
class PriorityQueue(Queue.PriorityQueue):
"""
Customised ``Queue.PriorityQueue``.
Each item in the queue must be tuple with three values. The first value
is the :class:`Image`'s ``priority`` attribute, the second value
the :class:`Image`'s ``secondary_priority`` attribute. The last value the
:class:`Image` instance itself::
(image.priority, image.secondary_priority, image)
Doing this, the :class:`Queue.PriorityQueue` will sort the images according
to their priorities, but also according to there number. However, the number
only has an impact on the result if there are more images with the same
priority. In such case the image which has been added earlier is privileged.
"""
def modify_priority(self, image, new_priority):
"""
Modifies the priority of the given ``image``.
``image``
The image to remove. This should be an ``Image`` instance.
The image to remove. This should be an :class:`Image` instance.
``new_priority``
The image's new priority.
The image's new priority. See the :class:`Priority` class for
priorities.
"""
self.remove(image)
image.priority = new_priority
self.put((image.priority, image))
self.put((image.priority, image.secondary_priority, image))
def remove(self, image):
"""
@ -135,8 +151,8 @@ class PriorityQueue(Queue.PriorityQueue):
``image``
The image to remove. This should be an ``Image`` instance.
"""
if (image.priority, image) in self.queue:
self.queue.remove((image.priority, image))
if (image.priority, image.secondary_priority, image) in self.queue:
self.queue.remove((image.priority, image.secondary_priority, image))
class ImageManager(QtCore.QObject):
@ -261,7 +277,8 @@ class ImageManager(QtCore.QObject):
if not name in self._cache:
image = Image(name, path, source, background)
self._cache[name] = image
self._conversion_queue.put((image.priority, image))
self._conversion_queue.put(
(image.priority, image.secondary_priority, image))
else:
log.debug(u'Image in cache %s:%s' % (name, path))
# We want only one thread.
@ -282,7 +299,7 @@ class ImageManager(QtCore.QObject):
Actually does the work.
"""
log.debug(u'_process_cache')
image = self._conversion_queue.get()[1]
image = self._conversion_queue.get()[2]
# Generate the QImage for the image.
if image.image is None:
image.image = resize_image(image.path, self.width, self.height,

View File

@ -235,8 +235,8 @@ class Renderer(object):
# the first two slides (and neglect the last for now).
if len(slides) == 3:
html_text = expand_tags(u'\n'.join(slides[:2]))
# We check both slides to determine if the optional break is
# needed (there is only one optional break).
# We check both slides to determine if the optional split is
# needed (there is only one optional split).
else:
html_text = expand_tags(u'\n'.join(slides))
html_text = html_text.replace(u'\n', u'<br>')
@ -247,14 +247,18 @@ class Renderer(object):
else:
# The first optional slide fits, which means we have to
# render the first optional slide.
text_contains_break = u'[---]' in text
if text_contains_break:
text_contains_split = u'[---]' in text
if text_contains_split:
try:
text_to_render, text = \
text.split(u'\n[---]\n', 1)
except:
text_to_render = text.split(u'\n[---]\n')[0]
text = u''
text_to_render, raw_tags, html_tags = \
self._get_start_tags(text_to_render)
if text:
text = raw_tags + text
else:
text_to_render = text
text = u''
@ -263,7 +267,7 @@ class Renderer(object):
if len(slides) > 1 and text:
# Add all slides apart from the last one the list.
pages.extend(slides[:-1])
if text_contains_break:
if text_contains_split:
text = slides[-1] + u'\n[---]\n' + text
else:
text = slides[-1] + u'\n'+ text
@ -492,7 +496,7 @@ class Renderer(object):
(raw_text.find(tag[u'start tag']), tag[u'start tag'],
tag[u'end tag']))
html_tags.append(
(raw_text.find(tag[u'start tag']), tag[u'start html']))
(raw_text.find(tag[u'start tag']), tag[u'start html']))
# Sort the lists, so that the tags which were opened first on the first
# slide (the text we are checking) will be opened first on the next
# slide as well.

View File

@ -57,6 +57,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
QtCore.SIGNAL(u'clicked()'), self.onDeleteClicked)
QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'),
self.close)
QtCore.QObject.connect(self.descriptionLineEdit,
QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
QtCore.QObject.connect(self.tagLineEdit,
QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
QtCore.QObject.connect(self.startTagLineEdit,
QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
QtCore.QObject.connect(self.endTagLineEdit,
QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
# Forces reloading of tags from openlp configuration.
FormattingTags.load_tags()
@ -65,7 +73,7 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
Load Display and set field state.
"""
# Create initial copy from master
self._resetTable()
self._reloadTable()
self.selected = -1
return QtGui.QDialog.exec_(self)
@ -73,9 +81,9 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
"""
Table Row selected so display items and set field state.
"""
row = self.tagTableWidget.currentRow()
html = FormattingTags.html_expands[row]
self.selected = row
self.savePushButton.setEnabled(False)
self.selected = self.tagTableWidget.currentRow()
html = FormattingTags.get_html_tags()[self.selected]
self.descriptionLineEdit.setText(html[u'desc'])
self.tagLineEdit.setText(self._strip(html[u'start tag']))
self.startTagLineEdit.setText(html[u'start html'])
@ -85,21 +93,26 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
self.tagLineEdit.setEnabled(False)
self.startTagLineEdit.setEnabled(False)
self.endTagLineEdit.setEnabled(False)
self.savePushButton.setEnabled(False)
self.deletePushButton.setEnabled(False)
else:
self.descriptionLineEdit.setEnabled(True)
self.tagLineEdit.setEnabled(True)
self.startTagLineEdit.setEnabled(True)
self.endTagLineEdit.setEnabled(True)
self.savePushButton.setEnabled(True)
self.deletePushButton.setEnabled(True)
def onTextEdited(self, text):
"""
Enable the ``savePushButton`` when any of the selected tag's properties
has been changed.
"""
self.savePushButton.setEnabled(True)
def onNewClicked(self):
"""
Add a new tag to list only if it is not a duplicate.
"""
for html in FormattingTags.html_expands:
for html in FormattingTags.get_html_tags():
if self._strip(html[u'start tag']) == u'n':
critical_error_message_box(
translate('OpenLP.FormattingTagForm', 'Update Error'),
@ -117,11 +130,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
u'temporary': False
}
FormattingTags.add_html_tags([tag])
self._resetTable()
FormattingTags.save_html_tags()
self._reloadTable()
# Highlight new row
self.tagTableWidget.selectRow(self.tagTableWidget.rowCount() - 1)
self.onRowSelected()
self.tagTableWidget.scrollToBottom()
#self.savePushButton.setEnabled(False)
def onDeleteClicked(self):
"""
@ -130,14 +145,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
if self.selected != -1:
FormattingTags.remove_html_tag(self.selected)
self.selected = -1
self._resetTable()
FormattingTags.save_html_tags()
FormattingTags.save_html_tags()
self._reloadTable()
def onSavedClicked(self):
"""
Update Custom Tag details if not duplicate and save the data.
"""
html_expands = FormattingTags.html_expands
html_expands = FormattingTags.get_html_tags()
if self.selected != -1:
html = html_expands[self.selected]
tag = unicode(self.tagLineEdit.text())
@ -157,14 +172,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog):
# Keep temporary tags when the user changes one.
html[u'temporary'] = False
self.selected = -1
self._resetTable()
FormattingTags.save_html_tags()
self._reloadTable()
def _resetTable(self):
def _reloadTable(self):
"""
Reset List for loading.
"""
FormattingTags.load_tags()
self.tagTableWidget.clearContents()
self.tagTableWidget.setRowCount(0)
self.newPushButton.setEnabled(True)

View File

@ -53,6 +53,7 @@ class WizardStrings(object):
OL = u'OpenLyrics'
OS = u'OpenSong'
OSIS = u'OSIS'
PS = u'PowerSong 1.0'
SB = u'SongBeamer'
SoF = u'Songs of Fellowship'
SSP = u'SongShow Plus'

View File

@ -171,6 +171,12 @@ class SongImportForm(OpenLPWizard):
QtCore.QObject.connect(self.foilPresenterRemoveButton,
QtCore.SIGNAL(u'clicked()'),
self.onFoilPresenterRemoveButtonClicked)
QtCore.QObject.connect(self.powerSongAddButton,
QtCore.SIGNAL(u'clicked()'),
self.onPowerSongAddButtonClicked)
QtCore.QObject.connect(self.powerSongRemoveButton,
QtCore.SIGNAL(u'clicked()'),
self.onPowerSongRemoveButtonClicked)
def addCustomPages(self):
"""
@ -217,6 +223,8 @@ class SongImportForm(OpenLPWizard):
self.addFileSelectItem(u'foilPresenter')
# Open Song
self.addFileSelectItem(u'openSong', u'OpenSong')
# PowerSong
self.addFileSelectItem(u'powerSong')
# SongBeamer
self.addFileSelectItem(u'songBeamer')
# Song Show Plus
@ -264,6 +272,8 @@ class SongImportForm(OpenLPWizard):
self.formatComboBox.setItemText(
SongFormat.FoilPresenter, WizardStrings.FP)
self.formatComboBox.setItemText(SongFormat.OpenSong, WizardStrings.OS)
self.formatComboBox.setItemText(
SongFormat.PowerSong, WizardStrings.PS)
self.formatComboBox.setItemText(
SongFormat.SongBeamer, WizardStrings.SB)
self.formatComboBox.setItemText(
@ -305,6 +315,10 @@ class SongImportForm(OpenLPWizard):
translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
self.dreamBeamRemoveButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
self.powerSongAddButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
self.powerSongRemoveButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
self.songsOfFellowshipAddButton.setText(
translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
self.songsOfFellowshipRemoveButton.setText(
@ -417,6 +431,12 @@ class SongImportForm(OpenLPWizard):
WizardStrings.YouSpecifyFile % WizardStrings.DB)
self.dreamBeamAddButton.setFocus()
return False
elif source_format == SongFormat.PowerSong:
if self.powerSongFileListWidget.count() == 0:
critical_error_message_box(UiStrings().NFSp,
WizardStrings.YouSpecifyFile % WizardStrings.PS)
self.powerSongAddButton.setFocus()
return False
elif source_format == SongFormat.SongsOfFellowship:
if self.songsOfFellowshipFileListWidget.count() == 0:
critical_error_message_box(UiStrings().NFSp,
@ -600,6 +620,22 @@ class SongImportForm(OpenLPWizard):
"""
self.removeSelectedItems(self.dreamBeamFileListWidget)
def onPowerSongAddButtonClicked(self):
"""
Get PowerSong song database files
"""
self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.PS,
self.powerSongFileListWidget, u'%s (*.song)'
% translate('SongsPlugin.ImportWizardForm',
'PowerSong 1.0 Song Files')
)
def onPowerSongRemoveButtonClicked(self):
"""
Remove selected PowerSong files from the import list
"""
self.removeSelectedItems(self.powerSongFileListWidget)
def onSongsOfFellowshipAddButtonClicked(self):
"""
Get Songs of Fellowship song database files
@ -717,6 +753,7 @@ class SongImportForm(OpenLPWizard):
self.wordsOfWorshipFileListWidget.clear()
self.ccliFileListWidget.clear()
self.dreamBeamFileListWidget.clear()
self.powerSongFileListWidget.clear()
self.songsOfFellowshipFileListWidget.clear()
self.genericFileListWidget.clear()
self.easySlidesFilenameEdit.setText(u'')
@ -784,6 +821,12 @@ class SongImportForm(OpenLPWizard):
filenames=self.getListOfFiles(
self.dreamBeamFileListWidget)
)
elif source_format == SongFormat.PowerSong:
# Import PowerSong songs
importer = self.plugin.importSongs(SongFormat.PowerSong,
filenames=self.getListOfFiles(
self.powerSongFileListWidget)
)
elif source_format == SongFormat.SongsOfFellowship:
# Import a Songs of Fellowship RTF file
importer = self.plugin.importSongs(SongFormat.SongsOfFellowship,

View File

@ -36,6 +36,7 @@ from openlyricsimport import OpenLyricsImport
from wowimport import WowImport
from cclifileimport import CCLIFileImport
from dreambeamimport import DreamBeamImport
from powersongimport import PowerSongImport
from ewimport import EasyWorshipSongImport
from songbeamerimport import SongBeamerImport
from songshowplusimport import SongShowPlusImport
@ -79,16 +80,17 @@ class SongFormat(object):
EasyWorship = 7
FoilPresenter = 8
OpenSong = 9
SongBeamer = 10
SongShowPlus = 11
SongsOfFellowship = 12
WordsOfWorship = 13
#CSV = 14
PowerSong = 10
SongBeamer = 11
SongShowPlus = 12
SongsOfFellowship = 13
WordsOfWorship = 14
#CSV = 15
@staticmethod
def get_class(format):
"""
Return the appropriate imeplementation class.
Return the appropriate implementation class.
``format``
The song format.
@ -111,6 +113,8 @@ class SongFormat(object):
return CCLIFileImport
elif format == SongFormat.DreamBeam:
return DreamBeamImport
elif format == SongFormat.PowerSong:
return PowerSongImport
elif format == SongFormat.EasySlides:
return EasySlidesImport
elif format == SongFormat.EasyWorship:
@ -139,6 +143,7 @@ class SongFormat(object):
SongFormat.EasyWorship,
SongFormat.FoilPresenter,
SongFormat.OpenSong,
SongFormat.PowerSong,
SongFormat.SongBeamer,
SongFormat.SongShowPlus,
SongFormat.SongsOfFellowship,

View File

@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2012 Raoul Snyman #
# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, #
# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias #
# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`powersongimport` module provides the functionality for importing
PowerSong songs into the OpenLP database.
"""
import logging
from openlp.core.lib import translate
from openlp.plugins.songs.lib.songimport import SongImport
log = logging.getLogger(__name__)
class PowerSongImport(SongImport):
"""
The :class:`PowerSongImport` class provides the ability to import song files
from PowerSong.
**PowerSong 1.0 Song File Format:**
The file has a number of label-field (think key-value) pairs.
Label and Field strings:
* Every label and field is a variable length string preceded by an
integer specifying it's byte length.
* Integer is 32-bit but is encoded in 7-bit format to save space. Thus
if length will fit in 7 bits (ie <= 127) it takes up only one byte.
Metadata fields:
* Every PowerSong file has a TITLE field.
* There is zero or more AUTHOR fields.
* There is always a COPYRIGHTLINE label, but its field may be empty.
This field may also contain a CCLI number: e.g. "CCLI 176263".
Lyrics fields:
* Each verse is contained in a PART field.
* Lines have Windows line endings ``CRLF`` (0x0d, 0x0a).
* There is no concept of verse types.
Valid extensions for a PowerSong song file are:
* .song
"""
def doImport(self):
"""
Receive a list of files to import.
"""
if not isinstance(self.importSource, list):
self.logError(unicode(translate('SongsPlugin.PowerSongImport',
'No files to import.')))
return
self.importWizard.progressBar.setMaximum(len(self.importSource))
for file in self.importSource:
if self.stopImportFlag:
return
self.setDefaults()
parse_error = False
with open(file, 'rb') as song_data:
while True:
try:
label = self._readString(song_data)
if not label:
break
field = self._readString(song_data)
except ValueError:
parse_error = True
self.logError(file, unicode(
translate('SongsPlugin.PowerSongImport',
'Invalid PowerSong file. Unexpected byte value.')))
break
else:
if label == u'TITLE':
self.title = field.replace(u'\n', u' ')
elif label == u'AUTHOR':
self.parseAuthor(field)
elif label == u'COPYRIGHTLINE':
found_copyright = True
self._parseCopyrightCCLI(field)
elif label == u'PART':
self.addVerse(field)
if parse_error:
continue
# Check that file had TITLE field
if not self.title:
self.logError(file, unicode(
translate('SongsPlugin.PowerSongImport',
'Invalid PowerSong file. Missing "TITLE" header.')))
continue
# Check that file had COPYRIGHTLINE label
if not found_copyright:
self.logError(file, unicode(
translate('SongsPlugin.PowerSongImport',
'"%s" Invalid PowerSong file. Missing "COPYRIGHTLINE" '
'header.' % self.title)))
continue
# Check that file had at least one verse
if not self.verses:
self.logError(file, unicode(
translate('SongsPlugin.PowerSongImport',
'"%s" Verses not found. Missing "PART" header.'
% self.title)))
continue
if not self.finish():
self.logError(file)
def _readString(self, file_object):
"""
Reads in next variable-length string.
"""
string_len = self._read7BitEncodedInteger(file_object)
return unicode(file_object.read(string_len), u'utf-8', u'ignore')
def _read7BitEncodedInteger(self, file_object):
"""
Reads in a 32-bit integer in compressed 7-bit format.
Accomplished by reading the integer 7 bits at a time. The high bit
of the byte when set means to continue reading more bytes.
If the integer will fit in 7 bits (ie <= 127), it only takes up one
byte. Otherwise, it may take up to 5 bytes.
Reference: .NET method System.IO.BinaryReader.Read7BitEncodedInt
"""
val = 0
shift = 0
i = 0
while True:
# Check for corrupted stream (since max 5 bytes per 32-bit integer)
if i == 5:
raise ValueError
byte = self._readByte(file_object)
# Strip high bit and shift left
val += (byte & 0x7f) << shift
shift += 7
high_bit_set = byte & 0x80
if not high_bit_set:
break
i += 1
return val
def _readByte(self, file_object):
"""
Reads in next byte as an unsigned integer
Note: returns 0 at end of file.
"""
byte_str = file_object.read(1)
# If read result is empty, then reached end of file
if not byte_str:
return 0
else:
return ord(byte_str)
def _parseCopyrightCCLI(self, field):
"""
Look for CCLI song number, and get copyright
"""
copyright, sep, ccli_no = field.rpartition(u'CCLI')
if not sep:
copyright = ccli_no
ccli_no = u''
if copyright:
self.addCopyright(copyright.rstrip(u'\n').replace(u'\n', u' '))
if ccli_no:
ccli_no = ccli_no.strip(u' :')
if ccli_no.isdigit():
self.ccliNumber = ccli_no

View File

@ -107,11 +107,11 @@ class SongImport(QtCore.QObject):
``filepath``
This should be the file path if ``self.importSource`` is a list
with different files. If it is not a list, but a single file (for
with different files. If it is not a list, but a single file (for
instance a database), then this should be the song's title.
``reason``
The reason, why the import failed. The string should be as
The reason why the import failed. The string should be as
informative as possible.
"""
self.setDefaults()

View File

@ -71,7 +71,7 @@ class WowImport(SongImport):
* ``SOH`` (0x01) - Chorus
* ``STX`` (0x02) - Bridge
Blocks are seperated by two bytes. The first byte is 0x01, and the
Blocks are separated by two bytes. The first byte is 0x01, and the
second byte is 0x80.
Lines:
@ -126,7 +126,7 @@ class WowImport(SongImport):
('Invalid Words of Worship song file. Missing '
'"CSongDoc::CBlock" string.'))))
continue
# Seek to the beging of the first block
# Seek to the beginning of the first block
song_data.seek(82)
for block in range(no_of_blocks):
self.linesToRead = ord(song_data.read(4)[:1])
@ -140,7 +140,7 @@ class WowImport(SongImport):
block_text += self.lineText
self.linesToRead -= 1
block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
# Blocks are seperated by 2 bytes, skip them, but not if
# Blocks are separated by 2 bytes, skip them, but not if
# this is the last block!
if block + 1 < no_of_blocks:
song_data.seek(2, os.SEEK_CUR)

View File

@ -33,7 +33,7 @@ The basic XML for storing the lyrics in the song database looks like this::
<song version="1.0">
<lyrics>
<verse type="c" label="1" lang="en">
<![CDATA[Chorus virtual slide 1[---]Chorus virtual slide 2]]>
<![CDATA[Chorus optional split 1[---]Chorus optional split 2]]>
</verse>
</lyrics>
</song>
@ -135,7 +135,7 @@ class SongXML(object):
The returned list has the following format::
[[{'type': 'v', 'label': '1'},
u"virtual slide 1[---]virtual slide 2"],
u"optional slide split 1[---]optional slide split 2"],
[{'lang': 'en', 'type': 'c', 'label': '1'}, u"English chorus"]]
"""
self.song_xml = None
@ -317,9 +317,7 @@ class OpenLyrics(object):
tags_element = None
match = re.search(u'\{/?\w+\}', song.lyrics, re.UNICODE)
if match:
# Reset available tags.
FormattingTags.reset_html_tags()
# Named 'formatting' - 'format' is built-in fuction in Python.
# Named 'format_' - 'format' is built-in fuction in Python.
format_ = etree.SubElement(song_xml, u'format')
tags_element = etree.SubElement(format_, u'tags')
tags_element.set(u'application', u'OpenLP')
@ -334,18 +332,59 @@ class OpenLyrics(object):
self._add_text_to_element(u'verse', lyrics, None, verse_def)
if u'lang' in verse[0]:
verse_element.set(u'lang', verse[0][u'lang'])
# Create a list with all "virtual" verses.
virtual_verses = cgi.escape(verse[1])
virtual_verses = virtual_verses.split(u'[---]')
for index, virtual_verse in enumerate(virtual_verses):
# Create a list with all "optional" verses.
optional_verses = cgi.escape(verse[1])
optional_verses = optional_verses.split(u'\n[---]\n')
start_tags = u''
end_tags = u''
for index, optional_verse in enumerate(optional_verses):
# Fix up missing end and start tags such as {r} or {/r}.
optional_verse = start_tags + optional_verse
start_tags, end_tags = self._get_missing_tags(optional_verse)
optional_verse += end_tags
# Add formatting tags to text
lines_element = self._add_text_with_tags_to_lines(verse_element,
virtual_verse, tags_element)
optional_verse, tags_element)
# Do not add the break attribute to the last lines element.
if index < len(virtual_verses) - 1:
if index < len(optional_verses) - 1:
lines_element.set(u'break', u'optional')
return self._extract_xml(song_xml)
def _get_missing_tags(self, text):
"""
Tests the given text for not closed formatting tags and returns a tuple
consisting of two unicode strings::
(u'{st}{r}', u'{/r}{/st}')
The first unicode string are the start tags (for the next slide). The
second unicode string are the end tags.
``text``
The text to test. The text must **not** contain html tags, only
OpenLP formatting tags are allowed::
{st}{r}Text text text
"""
tags = []
for tag in FormattingTags.get_html_tags():
if tag[u'start tag'] == u'{br}':
continue
if text.count(tag[u'start tag']) != text.count(tag[u'end tag']):
tags.append((text.find(tag[u'start tag']),
tag[u'start tag'], tag[u'end tag']))
# Sort the lists, so that the tags which were opened first on the first
# slide (the text we are checking) will be opened first on the next
# slide as well.
tags.sort(key=lambda tag: tag[0])
end_tags = []
start_tags = []
for tag in tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
return u''.join(start_tags), u''.join(end_tags)
def xml_to_song(self, xml, parse_and_temporary_save=False):
"""
Create and save a song from OpenLyrics format xml to the database. Since
@ -572,7 +611,8 @@ class OpenLyrics(object):
for tag in FormattingTags.get_html_tags()]
new_tags = [tag for tag in found_tags
if tag[u'start tag'] not in existing_tag_ids]
FormattingTags.add_html_tags(new_tags, True)
FormattingTags.add_html_tags(new_tags)
FormattingTags.save_html_tags()
def _process_lines_mixed_content(self, element, newlines=True):
"""