← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~mzibricky/openlp/openlyrics into lp:openlp

 

matysek has proposed merging lp:~mzibricky/openlp/openlyrics into lp:openlp.

Requested reviews:
  Raoul Snyman (raoul-snyman)
  Tim Bentley (trb143)
Related bugs:
  Bug #745636 in OpenLP: "OpenLyrics xml including OpenLP specifics"
  https://bugs.launchpad.net/openlp/+bug/745636

For more details, see:
https://code.launchpad.net/~mzibricky/openlp/openlyrics/+merge/74926

Added export to openlyrics format with formatting tags.

I removed:
- print statement
- db_file_path option

I'm now overriding data dir in AppLocation.BaseDir.
I consolidated the test code and made it more structured.
-- 
https://code.launchpad.net/~mzibricky/openlp/openlyrics/+merge/74926
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2011-09-04 20:00:10 +0000
+++ openlp/core/__init__.py	2011-09-11 21:57:25 +0000
@@ -228,26 +228,29 @@
         help='Set the Qt4 style (passed directly to Qt4).')
     parser.add_option('--testing', dest='testing',
         action='store_true', help='Run by testing framework')
-    # Set up logging
-    log_path = AppLocation.get_directory(AppLocation.CacheDir)
-    check_directory_exists(log_path)
-    filename = os.path.join(log_path, u'openlp.log')
-    logfile = logging.FileHandler(filename, u'w')
-    logfile.setFormatter(logging.Formatter(
-        u'%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
-    log.addHandler(logfile)
-    logging.addLevelName(15, u'Timer')
     # Parse command line options and deal with them.
     # Use args supplied programatically if possible.
     (options, args) = parser.parse_args(args) if args else parser.parse_args()
+    # Set up logging
+    # In test mode it is skipped
+    if not options.testing:
+        log_path = AppLocation.get_directory(AppLocation.CacheDir)
+        check_directory_exists(log_path)
+        filename = os.path.join(log_path, u'openlp.log')
+        logfile = logging.FileHandler(filename, u'w')
+        logfile.setFormatter(logging.Formatter(
+            u'%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
+        log.addHandler(logfile)
+        logging.addLevelName(15, u'Timer')
+        if options.loglevel.lower() in ['d', 'debug']:
+            log.setLevel(logging.DEBUG)
+            print 'Logging to:', filename
+        elif options.loglevel.lower() in ['w', 'warning']:
+            log.setLevel(logging.WARNING)
+        else:
+            log.setLevel(logging.INFO)
+    # Deal with other command line options.
     qt_args = []
-    if options.loglevel.lower() in ['d', 'debug']:
-        log.setLevel(logging.DEBUG)
-        print 'Logging to:', filename
-    elif options.loglevel.lower() in ['w', 'warning']:
-        log.setLevel(logging.WARNING)
-    else:
-        log.setLevel(logging.INFO)
     if options.style:
         qt_args.extend(['-style', options.style])
     # Throw the rest of the arguments at Qt, just in case.

=== modified file 'openlp/core/lib/formattingtags.py'
--- openlp/core/lib/formattingtags.py	2011-07-30 08:07:48 +0000
+++ openlp/core/lib/formattingtags.py	2011-09-11 21:57:25 +0000
@@ -27,6 +27,9 @@
 """
 Provide HTML Tag management and Formatting Tag access class
 """
+import cPickle
+
+from PyQt4 import QtCore
 
 from openlp.core.lib import translate
 
@@ -49,6 +52,8 @@
         """
         Resets the html_expands list.
         """
+        temporary_tags = [tag for tag in FormattingTags.html_expands
+            if tag[u'temporary']]
         FormattingTags.html_expands = []
         base_tags = []
         # Append the base tags.
@@ -56,75 +61,137 @@
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Red'),
             u'start tag': u'{r}',
             u'start html': u'<span style="-webkit-text-fill-color:red">',
-            u'end tag': u'{/r}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/r}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc':  translate('OpenLP.FormattingTags', 'Black'),
             u'start tag': u'{b}',
             u'start html': u'<span style="-webkit-text-fill-color:black">',
-            u'end tag': u'{/b}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/b}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Blue'),
             u'start tag': u'{bl}',
             u'start html': u'<span style="-webkit-text-fill-color:blue">',
-            u'end tag': u'{/bl}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/bl}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Yellow'),
             u'start tag': u'{y}',
             u'start html': u'<span style="-webkit-text-fill-color:yellow">',
-            u'end tag': u'{/y}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/y}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Green'),
             u'start tag': u'{g}',
             u'start html': u'<span style="-webkit-text-fill-color:green">',
-            u'end tag': u'{/g}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/g}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Pink'),
             u'start tag': u'{pk}',
             u'start html': u'<span style="-webkit-text-fill-color:#FFC0CB">',
-            u'end tag': u'{/pk}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/pk}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Orange'),
             u'start tag': u'{o}',
             u'start html': u'<span style="-webkit-text-fill-color:#FFA500">',
-            u'end tag': u'{/o}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/o}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Purple'),
             u'start tag': u'{pp}',
             u'start html': u'<span style="-webkit-text-fill-color:#800080">',
-            u'end tag': u'{/pp}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/pp}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'White'),
             u'start tag': u'{w}',
             u'start html': u'<span style="-webkit-text-fill-color:white">',
-            u'end tag': u'{/w}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/w}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Superscript'),
             u'start tag': u'{su}', u'start html': u'<sup>',
-            u'end tag': u'{/su}', u'end html': u'</sup>', u'protected': True})
+            u'end tag': u'{/su}', u'end html': u'</sup>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Subscript'),
             u'start tag': u'{sb}', u'start html': u'<sub>',
-            u'end tag': u'{/sb}', u'end html': u'</sub>', u'protected': True})
+            u'end tag': u'{/sb}', u'end html': u'</sub>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
             u'start tag': u'{p}', u'start html': u'<p>', u'end tag': u'{/p}',
-            u'end html': u'</p>', u'protected': True})
+            u'end html': u'</p>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Bold'),
             u'start tag': u'{st}', u'start html': u'<strong>',
             u'end tag': u'{/st}', u'end html': u'</strong>',
-            u'protected': True})
+            u'protected': True, u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Italics'),
             u'start tag': u'{it}', u'start html': u'<em>', u'end tag': u'{/it}',
-            u'end html': u'</em>', u'protected': True})
+            u'end html': u'</em>', u'protected': True, u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Underline'),
             u'start tag': u'{u}',
             u'start html': u'<span style="text-decoration: underline;">',
-            u'end tag': u'{/u}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/u}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Break'),
             u'start tag': u'{br}', u'start html': u'<br>', u'end tag': u'',
-            u'end html': u'', u'protected': True})
+            u'end html': u'', u'protected': True, u'temporary': False})
         FormattingTags.add_html_tags(base_tags)
-
-    @staticmethod
-    def add_html_tags(tags):
-        """
-        Add a list of tags to the list
+        FormattingTags.add_html_tags(temporary_tags)
+
+    @staticmethod
+    def save_html_tags():
+        """
+        Saves all formatting tags except protected ones.
+        """
+        tags = []
+        for tag in FormattingTags.get_html_tags():
+            if not tag[u'protected'] and not tag[u'temporary']:
+                tags.append(tag)
+        # 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 add_html_tags(tags, save=False):
+        """
+        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
+            The formatting tag's description, e. g. **Red**
+
+        * start tag
+            The start tag, e. g. ``{r}``
+
+        * end tag
+            The end tag, e. g. ``{/r}``
+
+        * start html
+            The start html tag. For instance ``<span style="
+            -webkit-text-fill-color:red">``
+
+        * end html
+            The end html tag. For example ``</span>``
+
+        * protected
+            A boolean stating whether this is a build-in tag or not. Should be
+            ``True`` in most cases.
+
+        * temporary
+            A temporary tag will not be saved, but is also considered when
+            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):

=== modified file 'openlp/core/ui/formattingtagform.py'
--- openlp/core/ui/formattingtagform.py	2011-07-30 07:43:19 +0000
+++ openlp/core/ui/formattingtagform.py	2011-09-11 21:57:25 +0000
@@ -132,7 +132,8 @@
             u'start html': translate('OpenLP.FormattingTagForm', '<HTML here>'),
             u'end tag': u'{/n}',
             u'end html': translate('OpenLP.FormattingTagForm', '</and here>'),
-            u'protected': False
+            u'protected': False,
+            u'temporary': False
         }
         FormattingTags.add_html_tags([tag])
         self._resetTable()
@@ -149,7 +150,7 @@
             FormattingTags.remove_html_tag(self.selected)
             self.selected = -1
         self._resetTable()
-        self._saveTable()
+        FormattingTags.save_html_tags()
 
     def onSavedPushed(self):
         """
@@ -172,21 +173,11 @@
             html[u'end html'] = unicode(self.endTagLineEdit.text())
             html[u'start tag'] = u'{%s}' % tag
             html[u'end tag'] = u'{/%s}' % tag
+            # Keep temporary tags when the user changes one.
+            html[u'temporary'] = False
             self.selected = -1
         self._resetTable()
-        self._saveTable()
-
-    def _saveTable(self):
-        """
-        Saves all formatting tags except protected ones.
-        """
-        tags = []
-        for tag in FormattingTags.get_html_tags():
-            if not tag[u'protected']:
-                tags.append(tag)
-        # Formatting Tags were also known as display tags.
-        QtCore.QSettings().setValue(u'displayTags/html_tags',
-            QtCore.QVariant(cPickle.dumps(tags) if tags else u''))
+        FormattingTags.save_html_tags()
 
     def _resetTable(self):
         """
@@ -198,8 +189,7 @@
         self.savePushButton.setEnabled(False)
         self.deletePushButton.setEnabled(False)
         for linenumber, html in enumerate(FormattingTags.get_html_tags()):
-            self.tagTableWidget.setRowCount(
-                self.tagTableWidget.rowCount() + 1)
+            self.tagTableWidget.setRowCount(self.tagTableWidget.rowCount() + 1)
             self.tagTableWidget.setItem(linenumber, 0,
                 QtGui.QTableWidgetItem(html[u'desc']))
             self.tagTableWidget.setItem(linenumber, 1,
@@ -208,6 +198,9 @@
                 QtGui.QTableWidgetItem(html[u'start html']))
             self.tagTableWidget.setItem(linenumber, 3,
                 QtGui.QTableWidgetItem(html[u'end html']))
+            # Tags saved prior to 1.9.7 do not have this key.
+            if not html.has_key(u'temporary'):
+                html[u'temporary'] = False
             self.tagTableWidget.resizeRowsToContents()
         self.descriptionLineEdit.setText(u'')
         self.tagLineEdit.setText(u'')

=== modified file 'openlp/core/utils/__init__.py'
--- openlp/core/utils/__init__.py	2011-08-12 14:54:16 +0000
+++ openlp/core/utils/__init__.py	2011-09-11 21:57:25 +0000
@@ -127,6 +127,9 @@
     CacheDir = 6
     LanguageDir = 7
 
+    # Base path where data/config/cache dir is located
+    BaseDir = None
+
     @staticmethod
     def get_directory(dir_type=1):
         """
@@ -152,6 +155,8 @@
                 os.path.abspath(os.path.split(sys.argv[0])[0]),
                 _get_os_dir_path(dir_type))
             return os.path.join(app_path, u'i18n')
+        elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
+            return os.path.join(AppLocation.BaseDir, 'data')
         else:
             return _get_os_dir_path(dir_type)
 

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2011-09-06 17:53:43 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2011-09-11 21:57:25 +0000
@@ -544,6 +544,9 @@
                 self._updateBackgroundAudio(song, item)
             editId = song.id
             self.onSearchTextButtonClick()
+        else:
+            # Make sure we temporary import formatting tags.
+            self.openLyrics.xml_to_song(item.xml_version, True)
         # Update service with correct song id.
         if editId:
             Receiver.send_message(u'service_item_update',

=== modified file 'openlp/plugins/songs/lib/xml.py'
--- openlp/plugins/songs/lib/xml.py	2011-08-28 20:51:44 +0000
+++ openlp/plugins/songs/lib/xml.py	2011-09-11 21:57:25 +0000
@@ -61,19 +61,18 @@
     </song>
 """
 
-import datetime
 import logging
 import re
 
 from lxml import etree, objectify
 
+from openlp.core.lib import FormattingTags
 from openlp.plugins.songs.lib import clean_song, VerseType
 from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
 from openlp.core.utils import get_application_version
 
 log = logging.getLogger(__name__)
 
-CHORD_REGEX = re.compile(u'<chord name=".*?"/>')
 
 class SongXML(object):
     """
@@ -202,7 +201,8 @@
         This property is not supported.
 
     ``<lines>``
-        The attribute *part* is not supported.
+        The attribute *part* is not supported. The *break* attribute is
+        supported.
 
     ``<publisher>``
         This property is not supported.
@@ -227,15 +227,35 @@
 
     ``<verse name="v1a" lang="he" translit="en">``
         The attribute *translit* is not supported. Note, the attribute *lang* is
-        considered, but there is not further functionality implemented yet.
+        considered, but there is not further functionality implemented yet. The
+        following verse "types" are supported by OpenLP:
+
+            * v
+            * c
+            * b
+            * p
+            * i
+            * e
+            * o
+
+        The verse "types" stand for *Verse*, *Chorus*, *Bridge*, *Pre-Chorus*,
+        *Intro*, *Ending* and *Other*. Any numeric value is allowed after the
+        verse type. The complete verse name in OpenLP always consists of the
+        verse type and the verse number. If not number is present *1* is
+        assumed.
+        OpenLP will merge verses which are split up by appending a letter to the
+        verse name, such as *v1a*.
 
     ``<verseOrder>``
         OpenLP supports this property.
 
     """
-    IMPLEMENTED_VERSION = u'0.7'
+    IMPLEMENTED_VERSION = u'0.8'
+
     def __init__(self, manager):
         self.manager = manager
+        self.start_tags_regex = re.compile(r'\{\w+\}')  # {abc}
+        self.end_tags_regex = re.compile(r'\{\/\w+\}')  # {/abc}
 
     def song_to_xml(self, song):
         """
@@ -249,8 +269,9 @@
         application_name = u'OpenLP ' + get_application_version()[u'version']
         song_xml.set(u'createdIn', application_name)
         song_xml.set(u'modifiedIn', application_name)
+        # "Convert" 2011-08-27 11:49:15 to 2011-08-27T11:49:15.
         song_xml.set(u'modifiedDate',
-            datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S'))
+            unicode(song.last_modified).replace(u' ', u'T'))
         properties = etree.SubElement(song_xml, u'properties')
         titles = etree.SubElement(properties, u'titles')
         self._add_text_to_element(u'title', titles, song.title)
@@ -284,29 +305,47 @@
             themes = etree.SubElement(properties, u'themes')
             for topic in song.topics:
                 self._add_text_to_element(u'theme', themes, topic.name)
+        # Process the formatting tags.
+        # have we any tags in song lyrics?
+        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
+            formatting = etree.SubElement(song_xml, u'format')
+            tags_element = etree.SubElement(formatting, u'tags')
+            tags_element.set(u'application', u'OpenLP')
         # Process the song's lyrics.
         lyrics = etree.SubElement(song_xml, u'lyrics')
         verse_list = sxml.get_verses(song.lyrics)
         for verse in verse_list:
             verse_tag = verse[0][u'type'][0].lower()
             verse_number = verse[0][u'label']
+            verse_def = verse_tag + verse_number
+            verse_element = \
+                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 = verse[1].split(u'[---]')
             for index, virtual_verse in enumerate(virtual_verses):
-                verse_def = verse_tag + verse_number
-                # We need "v1a" because we have more than one virtual verse.
-                if len(virtual_verses) > 1:
-                    verse_def += list(u'abcdefghijklmnopqrstuvwxyz')[index]
-                element = \
-                    self._add_text_to_element(u'verse', lyrics, None, verse_def)
-                if verse[0].has_key(u'lang'):
-                    element.set(u'lang', verse[0][u'lang'])
-                element = self._add_text_to_element(u'lines', element)
+                lines_element = \
+                    self._add_text_to_element(u'lines', verse_element)
+                # Do not add the break attribute to the last lines element.
+                if index < len(virtual_verses) - 1:
+                    lines_element.set(u'break', u'optional')
                 for line in virtual_verse.strip(u'\n').split(u'\n'):
-                    self._add_text_to_element(u'line', element, line)
+                    # Process only lines containing formatting tags
+                    if self.start_tags_regex.search(line):
+                        # add formatting tags to text
+                        self._add_line_with_tags_to_lines(lines_element, line,
+                            tags_element)
+                    else:
+                        self._add_text_to_element(u'line', lines_element, line)
         return self._extract_xml(song_xml)
 
-    def xml_to_song(self, xml):
+    def xml_to_song(self, xml, only_process_format_tags=False):
         """
         Create and save a song from OpenLyrics format xml to the database. Since
         we also export XML from external sources (e. g. OpenLyrics import), we
@@ -314,19 +353,25 @@
 
         ``xml``
             The XML to parse (unicode).
+
+        ``only_process_format_tags``
+            Switch to skip processing the whole song and to prevent storing the
+            songs to the database. Defaults to ``False``.
         """
         # No xml get out of here.
         if not xml:
             return None
         if xml[:5] == u'<?xml':
             xml = xml[38:]
-        # Remove chords from xml.
-        xml = CHORD_REGEX.sub(u'', xml)
         song_xml = objectify.fromstring(xml)
         if hasattr(song_xml, u'properties'):
             properties = song_xml.properties
         else:
             return None
+        if float(song_xml.get(u'version')) > 0.6:
+            self._process_formatting_tags(song_xml, only_process_format_tags)
+        if only_process_format_tags:
+            return
         song = Song()
         # Values will be set when cleaning the song.
         song.search_lyrics = u''
@@ -336,7 +381,7 @@
         self._process_cclinumber(properties, song)
         self._process_titles(properties, song)
         # The verse order is processed with the lyrics!
-        self._process_lyrics(properties, song_xml.lyrics, song)
+        self._process_lyrics(properties, song_xml, song)
         self._process_comments(properties, song)
         self._process_authors(properties, song)
         self._process_songbooks(properties, song)
@@ -355,6 +400,38 @@
         parent.append(element)
         return element
 
+    def _add_tag_to_formatting(self, tag_name, tags_element):
+        available_tags = FormattingTags.get_html_tags()
+        start_tag = '{%s}' % tag_name
+        for t in available_tags:
+            if t[u'start tag'] == start_tag:
+                # create new formatting tag in openlyrics xml
+                el = self._add_text_to_element(u'tag', tags_element)
+                el.set(u'name', tag_name)
+                el_open = self._add_text_to_element(u'open', el)
+                el_close = self._add_text_to_element(u'close', el)
+                el_open.text = etree.CDATA(t[u'start html'])
+                el_close.text = etree.CDATA(t[u'end html'])
+
+    def _add_line_with_tags_to_lines(self, parent, text, tags_element):
+        # tags already converted to xml structure
+        xml_tags = tags_element.xpath(u'tag/attribute::name')
+        start_tags = self.start_tags_regex.findall(text)
+        end_tags = self.end_tags_regex.findall(text)
+        # replace start tags with xml syntax
+        for tag in start_tags:
+            name = tag[1:-1]
+            text = text.replace(tag, u'<tag name="%s">' % name)
+            # add tag to <format> elment if tag not present
+            if name not in xml_tags:
+                self._add_tag_to_formatting(name, tags_element)
+        # replace end tags
+        for t in end_tags:
+            text = text.replace(t, u'</tag>')
+        text = u'<line>' + text + u'</line>'
+        element = etree.XML(text)
+        parent.append(element)
+
     def _extract_xml(self, xml):
         """
         Extract our newly created XML song.
@@ -362,20 +439,6 @@
         return etree.tostring(xml, encoding=u'UTF-8',
             xml_declaration=True)
 
-    def _get(self, element, attribute):
-        """
-        This returns the element's attribute as unicode string.
-
-        ``element``
-            The element.
-
-        ``attribute``
-            The element's attribute (unicode).
-        """
-        if element.get(attribute) is not None:
-            return unicode(element.get(attribute))
-        return u''
-
     def _text(self, element):
         """
         This returns the text of an element as unicode string.
@@ -457,29 +520,66 @@
         if hasattr(properties, u'copyright'):
             song.copyright = self._text(properties.copyright)
 
-    def _process_lyrics(self, properties, lyrics, song):
+    def _process_formatting_tags(self, song_xml, temporary):
+        """
+        Process the formatting tags from the song and either add missing tags
+        temporary or permanently to the formatting tag list.
+        """
+        if not hasattr(song_xml, u'format'):
+            return
+        found_tags = []
+        for tag in song_xml.format.tags.getchildren():
+            name = tag.get(u'name')
+            if name is None:
+                continue
+            openlp_tag = {
+                u'desc': name,
+                u'start tag': u'{%s}' % name[:5],
+                u'end tag': u'{/%s}' % name[:5],
+                u'start html': tag.open.text,
+                u'end html': tag.close.text,
+                u'protected': False,
+                u'temporary': temporary
+            }
+            found_tags.append(openlp_tag)
+        existing_tag_ids = [tag[u'start tag']
+            for tag in FormattingTags.get_html_tags()]
+        FormattingTags.add_html_tags([tag for tag in found_tags
+            if tag[u'start tag'] not in existing_tag_ids], True)
+
+    def _process_lyrics(self, properties, song_xml, song_obj):
         """
         Processes the verses and search_lyrics for the song.
 
         ``properties``
             The properties object (lxml.objectify.ObjectifiedElement).
 
-        ``lyrics``
-            The lyrics object (lxml.objectify.ObjectifiedElement).
+        ``song_xml``
+            The objectified song (lxml.objectify.ObjectifiedElement).
 
-        ``song``
+        ``song_obj``
             The song object.
         """
         sxml = SongXML()
         verses = {}
         verse_def_list = []
+        lyrics = song_xml.lyrics
+        # Loop over the "verse" elements.
         for verse in lyrics.verse:
             text = u''
+            # Loop over the "lines" elements.
             for lines in verse.lines:
                 if text:
                     text += u'\n'
-                text += u'\n'.join([unicode(line) for line in lines.line])
-            verse_def = self._get(verse, u'name').lower()
+                # Loop over the "line" elements removing chords.
+                for line in lines.line:
+                    if text:
+                        text += u'\n'
+                    text += u''.join(map(unicode, line.itertext()))
+                # Add a virtual split to the verse text.
+                if lines.get(u'break') is not None:
+                    text += u'\n[---]'
+            verse_def = verse.get(u'name', u' ').lower()
             if verse_def[0] in VerseType.Tags:
                 verse_tag = verse_def[0]
             else:
@@ -489,11 +589,16 @@
             # not correct the verse order.
             if not verse_number:
                 verse_number = u'1'
-            lang = None
-            if self._get(verse, u'lang'):
-                lang = self._get(verse, u'lang')
-            if verses.has_key((verse_tag, verse_number, lang)):
+            lang = verse.get(u'lang')
+            # In OpenLP 1.9.6 we used v1a, v1b ... to represent visual slide
+            # breaks. In OpenLyrics 0.7 an attribute has been added.
+            if song_xml.get(u'modifiedIn') in (u'1.9.6', u'OpenLP 1.9.6') and \
+                song_xml.get(u'version') == u'0.7' and \
+                (verse_tag, verse_number, lang) in verses:
                 verses[(verse_tag, verse_number, lang)] += u'\n[---]\n' + text
+            # Merge v1a, v1b, .... to v1.
+            elif (verse_tag, verse_number, lang) in verses:
+                verses[(verse_tag, verse_number, lang)] += u'\n' + text
             else:
                 verses[(verse_tag, verse_number, lang)] = text
                 verse_def_list.append((verse_tag, verse_number, lang))
@@ -501,10 +606,10 @@
         for verse in verse_def_list:
             sxml.add_verse_to_lyrics(
                 verse[0], verse[1], verses[verse], verse[2])
-        song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
+        song_obj.lyrics = unicode(sxml.extract_xml(), u'utf-8')
         # Process verse order
         if hasattr(properties, u'verseOrder'):
-            song.verse_order = self._text(properties.verseOrder)
+            song_obj.verse_order = self._text(properties.verseOrder)
 
     def _process_songbooks(self, properties, song):
         """
@@ -520,7 +625,7 @@
         song.song_number = u''
         if hasattr(properties, u'songbooks'):
             for songbook in properties.songbooks.songbook:
-                bookname = self._get(songbook, u'name')
+                bookname = songbook.get(u'name', u'')
                 if bookname:
                     book = self.manager.get_object_filtered(Book,
                         Book.name == bookname)
@@ -529,7 +634,7 @@
                         book = Book.populate(name=bookname, publisher=u'')
                         self.manager.save_object(book)
                     song.song_book_id = book.id
-                    song.song_number = self._get(songbook, u'entry')
+                    song.song_number = songbook.get(u'entry', u'')
                     # We only support one song book, so take the first one.
                     break
 

=== modified file 'testing/conftest.py'
--- testing/conftest.py	2011-09-02 11:15:41 +0000
+++ testing/conftest.py	2011-09-11 21:57:25 +0000
@@ -30,16 +30,137 @@
 Configuration file for pytest framework.
 """
 
+import os
+import sys
+import subprocess
+import logging
+
+import py.path
+from PyQt4 import QtCore
+from sqlalchemy.orm import clear_mappers
+
 from openlp.core import main as openlp_main
-
-
-# Test function argument to make openlp gui instance persistent for all tests.
-# All test cases have to access the same instance. To allow create multiple
-# instances it would be necessary use diffrent configuraion and data files.
-# Created instance will use your OpenLP settings.
-def pytest_funcarg__openlpapp(request):
-    def setup():
+from openlp.core.utils import AppLocation
+from openlp.core.lib.db import Manager
+from openlp.plugins.songs.lib.db import init_schema
+
+TESTS_PATH = os.path.dirname(os.path.abspath(__file__))
+
+RESOURCES_PATH = os.path.join(TESTS_PATH, 'resources')
+SONGS_PATH = os.path.join(RESOURCES_PATH, 'songs')
+
+
+# class to setup and teardown settings for running openlp tests
+class OpenLPRunner(object):
+    def __init__(self, tmpdir):
+        self.tmpdir = tmpdir
+        self._setup_qapp()
+        self._setup_logging()
+        self._cleanup_qsettings()
+        # override data dir of OpenLP - it points to tmpdir of a test case
+        AppLocation.BaseDir = tmpdir.strpath
+
+    def _setup_qapp(self):
+        QtCore.QCoreApplication.setOrganizationName(u'OpenLP')
+        QtCore.QCoreApplication.setOrganizationDomain(u'openlp.org')
+        QtCore.QCoreApplication.setApplicationName(u'TestOpenLP')
+
+    def _setup_logging(self):
+        # set up logging to stderr/stdout (console)
+        _handler = logging.StreamHandler(stream=None)
+        _handler.setFormatter(logging.Formatter(
+            u'%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
+        logging.addLevelName(15, u'Timer')
+        log = logging.getLogger()
+        log.addHandler(_handler)
+        log.setLevel(logging.DEBUG)
+
+    def _cleanup_qsettings(self):
+        # Clean up QSettings for all plugins
+        # The issue with QSettings is that is global for a running process
+        # and thus it is necessary to clean it before another test case.
+        # If it would not be cleaned up it could be saved in the system.
+        s = QtCore.QSettings()
+        keys = s.allKeys()
+        for k in keys:
+            s.setValue(k, None)
+
+    ## Public interface
+
+    def get_songs_db(self, empty=False):
+        # return initialized db Manager with empty db or db containing
+        # some example songs
+
+        if not empty:
+            # copy test data to tmpdir
+            datadir = self.tmpdir.mkdir(u'data').mkdir(u'songs')
+            orig_db = py.path.local(SONGS_PATH).join('songs.sqlite')
+            orig_db.copy(datadir)
+
+        manager = Manager('songs', init_schema)
+        return manager
+
+    def get_app(self):
+        # return QGui.QApplication of OpenLP - this object allows
+        # running different gui tests and allows access to gui objects
+        # (e.g MainWindow etc.)
+        # To allow creating multiple instances of OpenLP in one process
+        # it would be necessary use diffrent configuration and data files.
+        # Created instance will use your OpenLP settings.
         return openlp_main(['--testing'])
-    def teardown(app):
-        pass
-    return request.cached_setup(setup=setup, teardown=teardown, scope='session')
+
+    def teardown(self):
+        # clean up code to run after running the test case
+        self._cleanup_qsettings()
+        # sqlalchemy allows to map classess to only one database at a time
+        clear_mappers()
+        # set data dir to original value
+        AppLocation.BaseDir = None
+
+
+# Paths with resources for tests
+def pytest_funcarg__pth(request):
+    def setup():
+        class Pth(object):
+            def __init__(self):
+                self.tests = py.path.local(TESTS_PATH)
+                self.resources = py.path.local(RESOURCES_PATH)
+                self.songs = py.path.local(SONGS_PATH)
+        return Pth()
+    return request.cached_setup(setup=setup, scope='session')
+
+
+# Test function argument giving access to OpenLP runner
+def pytest_funcarg__openlp_runner(request):
+    def setup():
+        return OpenLPRunner(request.getfuncargvalue('tmpdir'))
+    def teardown(openlp_runner):
+        openlp_runner.teardown()
+    return request.cached_setup(setup=setup, teardown=teardown, scope='function')
+
+
+class OpenLyricsValidator(object):
+    """Validate xml if it conformns to OpenLyrics xml schema."""
+    def __init__(self, script, schema):
+            self.cmd = [sys.executable, script, schema]
+
+    def validate(self, file_path):
+        self.cmd.append(file_path)
+        print self.cmd
+        retcode = subprocess.call(self.cmd)
+        if retcode == 0:
+            # xml conforms to schema
+            return True
+        else:
+            # xml has invalid syntax
+            return False
+
+
+# Test function argument giving access to song database.
+def pytest_funcarg__openlyrics_validator(request):
+    def setup():
+        script = os.path.join(RESOURCES_PATH, 'openlyrics', 'validate.py')
+        schema = os.path.join(RESOURCES_PATH, 'openlyrics',
+            'openlyrics_schema.rng')
+        return OpenLyricsValidator(script, schema)
+    return request.cached_setup(setup=setup, scope='session')

=== added directory 'testing/resources'
=== added directory 'testing/resources/openlyrics'
=== added file 'testing/resources/openlyrics/openlyrics_schema.rng'
--- testing/resources/openlyrics/openlyrics_schema.rng	1970-01-01 00:00:00 +0000
+++ testing/resources/openlyrics/openlyrics_schema.rng	2011-09-11 21:57:25 +0000
@@ -0,0 +1,472 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0";
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes";
+         ns="http://openlyrics.info/namespace/2009/song";>
+
+  <!-- TOP LEVEL -->
+
+  <start>
+    <element name="song">
+      <ref name="songAttributes"/>
+      <ref name="properties"/>
+      <optional>
+        <ref name="format"/>
+      </optional>
+      <ref name="lyrics"/>
+    </element>
+  </start>
+
+  <define name="properties">
+    <element name="properties">
+      <interleave> <!-- allow occur in any order -->
+        <!-- at least one title is always required -->
+        <ref name="titles"/>
+        <!-- other properties items are optional -->
+        <optional>
+          <ref name="authors"/>
+        </optional>
+        <optional>
+          <ref name="copyright"/>
+        </optional>
+        <optional>
+          <ref name="ccliNo"/>
+        </optional>
+        <optional>
+          <ref name="releaseDate"/>
+        </optional>
+        <!-- Music Info -->
+        <optional>
+          <ref name="transposition"/>
+        </optional>
+        <optional>
+          <ref name="tempo"/>
+        </optional>
+        <optional>
+          <ref name="key"/>
+        </optional>
+        <!-- Other Info -->
+        <optional>
+          <ref name="variant"/>
+        </optional>
+        <optional>
+          <ref name="publisher"/>
+        </optional>
+        <optional>
+          <ref name="customVersion"/>
+        </optional>
+        <optional>
+          <ref name="keywords"/>
+        </optional>
+        <optional>
+          <ref name="verseOrder"/>
+        </optional>
+        <optional>
+          <ref name="songbooks"/>
+        </optional>
+        <optional>
+          <ref name="themes"/>
+        </optional>
+        <optional>
+          <ref name="comments"/>
+        </optional>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="format">
+    <element name="format">
+        <ref name="formatTags"/>
+    </element>
+  </define>
+
+
+  <define name="lyrics">
+    <element name="lyrics">
+      <!-- at least one verse is required -->
+      <oneOrMore>
+        <ref name="verse"/>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <!-- PROPERTIES -->
+
+  <define name="titles">
+    <element name="titles">
+      <oneOrMore>
+        <element name="title">
+          <ref name="nonEmptyContent"/>
+          <optional>
+            <ref name="langAttribute"/>
+            <optional>
+              <ref name="translitAttribute"/>
+            </optional>
+          </optional>
+          <optional>
+            <attribute name="original">
+              <data type="boolean"/>
+            </attribute>
+          </optional>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <!-- AUTHOR info -->
+
+  <define name="authors">
+    <element name="authors">
+      <oneOrMore>
+        <element name="author">
+          <ref name="nonEmptyContent"/>
+          <optional>
+            <choice>
+              <attribute name="type">
+                <choice>
+                  <value>words</value>
+                  <value>music</value>
+                </choice>
+              </attribute>
+              <!-- when attrib 'type' value is 'translation' require attribute 'lang'.
+                   'xml:lang' can't be used. xml:lang means in what language is the
+                   content of an element and this is not the case. -->
+              <group>
+                <attribute name="type">
+                  <value>translation</value>
+                </attribute>
+                <ref name="langAttribute"/>
+              </group>
+            </choice>
+          </optional>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <define name="copyright">
+    <element name="copyright">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+
+  <define name="ccliNo">
+    <element name="ccliNo">
+      <data type="positiveInteger"/>
+    </element>
+  </define>
+
+  <define name="releaseDate">
+    <element name="releaseDate">
+      <!-- allowed values
+           1779
+           1779-12
+           1779-12-31
+           1779-12-31T13:15:30+01:00 -->
+      <choice>
+        <data type="gYear"/>
+        <data type="gYearMonth"/>
+        <data type="date"/>
+        <data type="dateTime"/>
+      </choice>
+    </element>
+  </define>
+
+  <!-- MUSIC INFO -->
+
+  <define name="transposition">
+    <element name="transposition">
+      <data type="integer">
+        <param name="minInclusive">-99</param>
+        <param name="maxInclusive">99</param>
+      </data>
+    </element>
+  </define>
+
+  <define name="tempo">
+    <element name="tempo">
+      <choice>
+        <!-- attrib 'type' value 'bpm' - beatss per minute required -->
+        <group>
+          <data type="positiveInteger">
+            <param name="minInclusive">30</param>
+            <param name="maxInclusive">250</param>
+          </data>
+          <attribute name="type">
+            <value>bpm</value>
+          </attribute>
+        </group>
+        <!-- attrib 'type' value 'text' - any text -->
+        <group>
+          <ref name="nonEmptyContent"/>
+          <attribute name="type">
+            <value>text</value>
+          </attribute>
+        </group>
+      </choice>
+    </element>
+  </define>
+ 
+
+  <define name="key">
+    <element name="key">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+ 
+  <!-- OTHER INFO --> 
+
+  <define name="variant">
+    <element name="variant">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+
+  <define name="publisher">
+    <element name="publisher">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+
+  <define name="customVersion">
+    <element name="customVersion">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+
+  <define name="keywords">
+    <element name="keywords">
+      <ref name="nonEmptyContent"/>
+    </element>
+  </define>
+
+  <define name="verseOrder">
+    <element name="verseOrder">
+      <list>
+        <oneOrMore>
+          <ref name="verseNameType"/>
+        </oneOrMore>
+      </list>
+    </element>
+  </define>
+
+  <define name="songbooks">
+    <element name="songbooks">
+      <oneOrMore>
+        <element name="songbook">
+          <attribute name="name">
+            <ref name="nonEmptyContent"/>
+          </attribute>
+          <optional>
+            <!-- 'entry' is like song number but song number must not
+                 always be integer and it can contain letters.
+                 examples: '153c' or '023', etc. -->
+            <attribute name="entry">
+              <ref name="nonEmptyContent"/>
+            </attribute>
+          </optional>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <define name="themes">
+    <element name="themes">
+      <oneOrMore>
+        <element name="theme">
+          <ref name="nonEmptyContent"/>
+          <optional>
+            <!-- id: line in a ccli theme list from
+                 http://www.ccli.com.au/owners/themes.cfm -->
+            <attribute name="id">
+              <data type="positiveInteger">
+                <param name="minInclusive">1</param>
+                <param name="maxInclusive">999</param>
+              </data>
+            </attribute>
+          </optional>
+          <optional>
+            <ref name="langAttribute"/>
+            <optional>
+              <ref name="translitAttribute"/>
+            </optional>
+          </optional>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <define name="comments">
+    <element name="comments">
+      <oneOrMore>
+        <element name="comment">
+          <ref name="nonEmptyContent"/>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <!-- FORMAT -->
+
+  <define name="formatTags">
+    <!-- Allow only one set of formatting tags for lyrics -->
+    <element name="tags">
+      <attribute name="application">
+        <ref name="nonEmptyContent"/>
+      </attribute>
+      <oneOrMore>
+        <ref name="formatTagsTag"/>
+      </oneOrMore>
+    </element>
+  </define>
+
+  <define name="formatTagsTag">
+    <element name="tag">
+      <attribute name="name">
+        <ref name="nonEmptyContent"/>
+      </attribute>
+      <element name="open">
+        <ref name="nonEmptyContent"/>
+      </element>
+      <element name="close">
+        <ref name="nonEmptyContent"/>
+      </element>
+    </element>
+  </define>
+
+ <!-- LYRICS -->
+
+  <define name="verse">
+    <element name="verse">
+      <ref name="verseAttributes"/>
+      <optional>
+        <ref name="langAttribute"/>
+        <optional>
+          <ref name="translitAttribute"/>
+        </optional>
+      </optional>
+      <oneOrMore>
+        <ref name="lines"/>
+      </oneOrMore>
+    </element>
+  </define>
+ 
+  <define name="lines">
+    <element name="lines">
+      <optional>
+        <attribute name="part">
+          <ref name="nonEmptyContent"/>
+        </attribute>
+      </optional>
+      <optional>
+        <attribute name="break">
+          <value>optional</value>
+        </attribute>
+      </optional>
+      <oneOrMore>
+        <optional>
+          <element name="comment">
+            <ref name="nonEmptyContent"/>
+          </element>
+        </optional>
+        <element name="line">
+          <!-- allow tag chord inside regular text - mixed content -->
+          <zeroOrMore>
+            <optional>
+              <ref name="chord"/>
+            </optional>
+            <!-- allow tag 'tag' inside regular text - mixed content -->
+            <optional>
+              <ref name="tag"/>
+            </optional>
+            <text/>
+          </zeroOrMore>
+        </element>
+      </oneOrMore>
+    </element>
+  </define>
+ 
+  <define name="chord">
+    <element name="chord">
+      <attribute name="name">
+        <ref name="nonEmptyContent"/>
+      </attribute>
+      <empty/>
+    </element>
+  </define>
+ 
+  <define name="tag">
+    <element name="tag">
+      <attribute name="name">
+        <ref name="nonEmptyContent"/>
+      </attribute>
+      <!-- allow using more formatting tags for text -->
+      <!-- e.g. <tag name="bold"><tag name="red">my text</tag></tag> -->
+      <choice>
+        <ref name="nonEmptyContent"/>
+        <ref name="tag"/>
+      </choice>
+    </element>
+  </define>
+ 
+  <define name="verseAttributes">
+    <attribute name="name">
+      <ref name="verseNameType"/>
+    </attribute>
+  </define>
+ 
+  <define name="songAttributes">
+    <!-- by default: value of type string is required in attr -->     
+    <attribute name="version">
+      <data type="NMTOKEN"> <!-- one word value -->
+        <!-- allow only values like: '0.1' '11.2' '13.14.15' -->
+        <param name="pattern">[0-9]+\.[0-9]+(\.[0-9]+)?</param>
+      </data>
+    </attribute>
+    <attribute name="createdIn">
+      <ref name="nonEmptyContent"/>
+    </attribute>
+    <attribute name="modifiedIn">
+      <ref name="nonEmptyContent"/>
+    </attribute>
+    <attribute name="modifiedDate">
+      <!-- date format: ISO 8601 -->
+      <data type="dateTime"/>
+    </attribute>
+  </define>
+
+  <define name="verseNameType">
+    <choice>
+      <data type="NMTOKEN">
+        <param name="minLength">1</param>
+        <!-- verse - v1, v2, v1a, ...  3 letters: [verse][verse_number][verse_part]
+             chorus c, c1, c2, c1a, ca, ...
+             pre-chorus - p, p1, p2, p1a, pa, ...
+             bridge - b, b1, b2, b1a, ba, ...
+             ending - e, e1, e2, e1a, ea, ... -->
+        <param name="pattern">(v[1-9]\d?[a-z]?)|([cpb][a-z]?)|([cpbe][1-9]\d?[a-z]?)</param>
+      </data>
+      <!-- custom values of verse name - one word name -->
+      <data type="NMTOKEN"/>
+    </choice>
+  </define>
+
+  <define name="langAttribute">
+    <attribute name="lang">
+      <data type="language"/>
+    </attribute>
+  </define>
+
+  <!-- transliteration -->
+  <define name="translitAttribute">
+    <attribute name="translit">
+      <data type="language"/>
+    </attribute>
+  </define>
+
+  <define name="nonEmptyContent">
+    <data type="string">
+      <param name="minLength">1</param>
+    </data>
+  </define>
+
+</grammar>

=== added file 'testing/resources/openlyrics/validate.py'
--- testing/resources/openlyrics/validate.py	1970-01-01 00:00:00 +0000
+++ testing/resources/openlyrics/validate.py	2011-09-11 21:57:25 +0000
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+import sys
+
+try:
+    from lxml import etree
+except ImportError:
+    print('Python module "lxml" is required')
+    exit(1)
+
+
+if len(sys.argv) != 3:
+    print('Usage: python   %s   openlyrics_schema.rng  xmlfile.xml' % __file__)
+    exit(1)
+
+
+relaxng_file = sys.argv[1]
+xml_file = sys.argv[2]
+
+relaxng_doc = etree.parse(relaxng_file)
+xml_doc = etree.parse(xml_file)
+
+relaxng = etree.RelaxNG(relaxng_doc)
+
+relaxng.assertValid(xml_doc)
+

=== added directory 'testing/resources/songs'
=== added file 'testing/resources/songs/openlyrics_test_1.xml'
--- testing/resources/songs/openlyrics_test_1.xml	1970-01-01 00:00:00 +0000
+++ testing/resources/songs/openlyrics_test_1.xml	2011-09-11 21:57:25 +0000
@@ -0,0 +1,102 @@
+<?xml version='1.0' encoding='utf-8'?>
+<song xmlns="http://openlyrics.info/namespace/2009/song"; version="0.8" createdIn="OpenLP 1.9.5" modifiedIn="OpenLP 1.9.5" modifiedDate="2011-09-06T20:49:59">
+  <properties>
+    <titles>
+      <title>Jezu Kriste, štědrý kněže</title>
+    </titles>
+    <authors>
+      <author>M. Jan Hus</author>
+    </authors>
+    <songbooks>
+      <songbook name="Jistebnický kancionál"/>
+    </songbooks>
+  </properties>
+  <format>
+    <tags application="OpenLP">
+      <tag name="r">
+        <open>&lt;span style="-webkit-text-fill-color:red"&gt;</open>
+        <close>&lt;/span&gt;</close>
+      </tag>
+      <tag name="bl">
+        <open>&lt;span style="-webkit-text-fill-color:blue"&gt;</open>
+        <close>&lt;/span&gt;</close>
+      </tag>
+      <tag name="y">
+        <open>&lt;span style="-webkit-text-fill-color:yellow"&gt;</open>
+        <close>&lt;/span&gt;</close>
+      </tag>
+      <tag name="o">
+        <open>&lt;span style="-webkit-text-fill-color:#FFA500"&gt;</open>
+        <close>&lt;/span&gt;</close>
+      </tag>
+      <tag name="st">
+        <open>&lt;strong&gt;</open>
+        <close>&lt;/strong&gt;</close>
+      </tag>
+      <tag name="it">
+        <open>&lt;em&gt;</open>
+        <close>&lt;/em&gt;</close>
+      </tag>
+      <tag name="g">
+        <open>&lt;span style="-webkit-text-fill-color:green"&gt;</open>
+        <close>&lt;/span&gt;</close>
+      </tag>
+    </tags>
+  </format>
+  <lyrics>
+    <verse name="v1">
+      <lines>
+        <line><tag name="r">Jezu Kriste</tag>, štědrý kněže,</line>
+        <line>s <tag name="bl">Otcem, Duchem</tag> jeden <tag name="y">Bože</tag>,</line>
+        <line>štědrost Tvá je naše zboží,</line>
+        <line>z <tag name="o"><tag name="st">Tvé</tag></tag> <tag name="it">milosti</tag>.</line>
+      </lines>
+    </verse>
+    <verse name="v2">
+      <lines>
+        <line><tag name="bl">Ty</tag> jsi v světě, bydlil s námi,</line>
+        <line>Tvé tělo trpělo rány</line>
+        <line>za nás za hříšné křesťany,</line>
+        <line>z <tag name="bl">Tvé</tag> milosti.</line>
+      </lines>
+    </verse>
+    <verse name="v3">
+      <lines>
+        <line>Ó, <tag name="g">Tvá dobroto</tag> důstojná</line>
+        <line>a k nám milosti přehojná!</line>
+        <line>Dáváš nám bohatství mnohá</line>
+        <line>
+          <tag name="st">
+            <tag name="y">z Tvé milosti.</tag>
+          </tag>
+        </line>
+      </lines>
+    </verse>
+    <verse name="v4">
+      <lines>
+        <line>Ráčils nás sám zastoupiti,</line>
+        <line>
+          <tag name="it">život za nás položiti,</tag>
+        </line>
+        <line>tak smrt věčnou zahladiti,</line>
+        <line>z Tvé milosti.</line>
+      </lines>
+    </verse>
+    <verse name="v5">
+      <lines>
+        <line>Ó, křesťané, z bludů vstaňme,</line>
+        <line>dané dobro nám poznejme,</line>
+        <line>k Synu Božímu chvátejme,</line>
+        <line>k té milosti!</line>
+      </lines>
+    </verse>
+    <verse name="v6">
+      <lines>
+        <line>Chvála budiž Bohu Otci,</line>
+        <line>Synu jeho téže moci,</line>
+        <line>Duchu jeho rovné moci,</line>
+        <line>z též milosti!</line>
+      </lines>
+    </verse>
+  </lyrics>
+</song>

=== added file 'testing/resources/songs/songs.sqlite'
Binary files testing/resources/songs/songs.sqlite	1970-01-01 00:00:00 +0000 and testing/resources/songs/songs.sqlite	2011-09-11 21:57:25 +0000 differ
=== modified file 'testing/test_app.py'
--- testing/test_app.py	2011-09-02 11:15:41 +0000
+++ testing/test_app.py	2011-09-11 21:57:25 +0000
@@ -26,11 +26,18 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
+"""
+GUI tests
+"""
+
 from openlp.core import OpenLP
 from openlp.core.ui.mainwindow import MainWindow
 
 
-def test_start_app(openlpapp):
-    assert type(openlpapp) == OpenLP
-    assert type(openlpapp.mainWindow) == MainWindow
-    assert unicode(openlpapp.mainWindow.windowTitle()) == u'OpenLP 2.0'
+# TODO Uncommend when using custom OpenLP configuration is implemented.
+# Otherwise it would mess up user's OpenLP settings
+#def test_start_app(openlp_runner):
+    #app = openlp_runner.get_app()
+    #assert type(app) == OpenLP
+    #assert type(app.mainWindow) == MainWindow
+    #assert unicode(app.mainWindow.windowTitle()) == u'OpenLP 2.0'

=== added file 'testing/test_openlyrics.py'
--- testing/test_openlyrics.py	1970-01-01 00:00:00 +0000
+++ testing/test_openlyrics.py	2011-09-11 21:57:25 +0000
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan,      #
+# Armin Köhler, Joshua Millar, 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                          #
+###############################################################################
+
+"""
+OpenLyrics import/export tests
+"""
+
+from lxml import etree
+
+from openlp.plugins.songs.lib.db import Song
+from openlp.plugins.songs.lib import OpenLyrics
+
+
+def test_openlyrics_export(openlp_runner, openlyrics_validator, pth, tmpdir):
+    # export song to file
+    f = tmpdir.join('out.xml')
+    db = openlp_runner.get_songs_db()
+    s = db.get_all_objects(Song)[0]
+    o = OpenLyrics(db)
+    xml = o.song_to_xml(s)
+    tree = etree.ElementTree(etree.fromstring(xml))
+    tree.write(open(f.strpath, u'w'), encoding=u'utf-8', xml_declaration=True,
+        pretty_print=True)
+    # validate file
+    assert openlyrics_validator.validate(f.strpath) == True
+    # string comparison with original file line by line
+    f_orig = pth.songs.join('openlyrics_test_1.xml')
+    for l, l_orig in zip(f.readlines(), f_orig.readlines()):
+        # skip line with item modifiedDate - it is unique everytime
+        if l.startswith('<song xmlns='):
+            continue
+        assert l == l_orig

=== added file 'testing/test_songs_db.py'
--- testing/test_songs_db.py	1970-01-01 00:00:00 +0000
+++ testing/test_songs_db.py	2011-09-11 21:57:25 +0000
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan,      #
+# Armin Köhler, Joshua Millar, 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                          #
+###############################################################################
+
+"""
+Songs database tests
+"""
+
+import pytest
+from sqlalchemy.exc import InvalidRequestError
+from sqlalchemy.orm.exc import UnmappedInstanceError
+
+from openlp.plugins.songs.lib.db import Author, Book, MediaFile, Song, Topic
+
+
+def test_empty_songdb(openlp_runner):
+    db = openlp_runner.get_songs_db(empty=True)
+    g = db.get_all_objects
+    assert g(Author) == []
+    assert g(Book) == []
+    assert g(MediaFile) == []
+    assert g(Song) == []
+    assert g(Topic) == []
+    c = db.get_object_count
+    assert c(Author) == 0
+    assert c(Book) == 0
+    assert c(MediaFile) == 0
+    assert c(Song) == 0
+    assert c(Topic) == 0
+
+
+def test_unmapped_class(openlp_runner):
+    # test class not mapped to any sqlalchemy table
+    class A(object):
+        pass
+    db = openlp_runner.get_songs_db(empty=True)
+    assert db.save_object(A()) == False
+    assert db.save_objects([A(), A()]) == False
+    # no key - new object instance is created from supplied class
+    assert type(db.get_object(A, key=None)) == A
+
+    with pytest.raises(InvalidRequestError):
+        db.get_object(A, key=1)
+    with pytest.raises(InvalidRequestError):
+        db.get_object_filtered(A, filter_clause=None)
+    with pytest.raises(InvalidRequestError):
+        db.get_all_objects(A)
+    with pytest.raises(InvalidRequestError):
+        db.get_object_count(A)
+
+    assert db.delete_object(A, key=None) == False
+    assert db.delete_all_objects(A) == False


Follow ups