← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/media into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/media into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #885150 in OpenLP: "Need non self contained service files"
  https://bugs.launchpad.net/openlp/+bug/885150
  Bug #899714 in OpenLP: "Play/Pause button should be merged"
  https://bugs.launchpad.net/openlp/+bug/899714
  Bug #927829 in OpenLP: "media backends should provide some information about themselves in the settings"
  https://bugs.launchpad.net/openlp/+bug/927829
  Bug #952821 in OpenLP: "Unable to play videos from web"
  https://bugs.launchpad.net/openlp/+bug/952821
  Bug #958198 in OpenLP: "Replacing live background with a video shows theme behind"
  https://bugs.launchpad.net/openlp/+bug/958198
  Bug #999618 in OpenLP: "Video position slider jumps to part way through video"
  https://bugs.launchpad.net/openlp/+bug/999618
  Bug #1022053 in OpenLP: "Previewing media item interferes with live media item"
  https://bugs.launchpad.net/openlp/+bug/1022053
  Bug #1063211 in OpenLP: "Media and Presentation Plugins do not update the service suffix lists if players are added or removed without a restart"
  https://bugs.launchpad.net/openlp/+bug/1063211

For more details, see:
https://code.launchpad.net/~trb143/openlp/media/+merge/141492

Clean up some fallout from the reformatting updates
Add a custom media slider with hover goodies and move to click
Add some new tests for service item.
Re-factor and firm up the service item validation code in service manager. 
-- 
https://code.launchpad.net/~trb143/openlp/media/+merge/141492
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/media into lp:openlp.
=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2012-12-28 22:06:43 +0000
+++ openlp/core/lib/serviceitem.py	2012-12-30 19:59:23 +0000
@@ -182,6 +182,7 @@
         self.theme_overwritten = False
         self.temporary_edit = False
         self.will_auto_start = False
+        self.has_original_files = True
         self._new_item()
 
     def _new_item(self):
@@ -190,6 +191,7 @@
         service items to see if they are the same.
         """
         self._uuid = unicode(uuid.uuid1())
+        self.validate_item()
 
     def add_capability(self, capability):
         """
@@ -395,6 +397,7 @@
         self.end_time = header.get(u'end_time', 0)
         self.media_length = header.get(u'media_length', 0)
         self.will_auto_start = header.get(u'will_auto_start', False)
+        self.has_original_files = True
         if u'background_audio' in header:
             self.background_audio = []
             for filename in header[u'background_audio']:
@@ -406,6 +409,7 @@
                 self._raw_frames.append(slide)
         elif self.service_item_type == ServiceItemType.Image:
             if path:
+                self.has_original_files = False
                 for text_image in serviceitem[u'serviceitem'][u'data']:
                     filename = os.path.join(path, text_image)
                     self.add_from_image(filename, text_image)
@@ -415,6 +419,7 @@
         elif self.service_item_type == ServiceItemType.Command:
             for text_image in serviceitem[u'serviceitem'][u'data']:
                 if path:
+                    self.has_original_files = False
                     self.add_from_command(path, text_image[u'title'], text_image[u'image'])
                 else:
                     self.add_from_command(text_image[u'path'], text_image[u'title'], text_image[u'image'])
@@ -605,8 +610,25 @@
                 if self.get_frame_path(frame=frame) in invalid_paths:
                     self.remove_frame(frame)
 
-    def validate(self):
-        """
-        Validates this service item
-        """
-        return bool(self._raw_frames)
+    def missing_frames(self):
+        """
+        Returns if there are any frames in the service item
+        """
+        return not bool(self._raw_frames)
+
+    def validate_item(self, suffix_list=None):
+        """
+        Validates a service item to make sure it is valid
+        """
+        self.is_valid = True
+        for frame in self._raw_frames:
+            if self.is_image() and not os.path.exists((frame[u'path'])):
+                self.is_valid = False
+            elif self.is_command():
+                file = os.path.join(frame[u'path'],frame[u'title'])
+                if not os.path.exists(file):
+                    self.is_valid = False
+                if suffix_list:
+                    type = frame[u'title'].split(u'.')[-1]
+                    if type.lower() not in suffix_list:
+                        self.is_valid = False

=== modified file 'openlp/core/ui/exceptiondialog.py'
--- openlp/core/ui/exceptiondialog.py	2012-12-29 09:35:24 +0000
+++ openlp/core/ui/exceptiondialog.py	2012-12-30 19:59:23 +0000
@@ -29,7 +29,7 @@
 
 from PyQt4 import QtCore, QtGui
 
-from openlp.core.lib import translate, build_icon
+from openlp.core.lib import translate
 from openlp.core.lib.ui import create_button, create_button_box
 
 class Ui_ExceptionDialog(object):

=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py	2012-12-27 16:27:59 +0000
+++ openlp/core/ui/media/mediacontroller.py	2012-12-30 19:59:23 +0000
@@ -29,19 +29,49 @@
 
 import logging
 import os
-import sys
+import datetime
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import OpenLPToolbar, Receiver, translate, Settings
 from openlp.core.lib.ui import UiStrings, critical_error_message_box
-from openlp.core.ui.media import MediaState, MediaInfo, MediaType, \
-    get_media_players, set_media_players
+from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players
 from openlp.core.ui.media.mediaplayer import MediaPlayer
 from openlp.core.utils import AppLocation
 from openlp.core.ui import DisplayControllerType
 
 log = logging.getLogger(__name__)
 
+class MediaSlider(QtGui.QSlider):
+    """
+    Allows the mouse events of a slider to be overridden and extra functionality added
+    """
+    def __init__(self, direction, manager, controller, parent=None):
+        QtGui.QSlider.__init__(self, direction)
+        self.manager = manager
+        self.controller = controller
+
+    def mouseMoveEvent(self, event):
+        """
+        Override event to allow hover time to be displayed.
+        """
+        timevalue = QtGui.QStyle.sliderValueFromPosition(self.minimum(),self.maximum(),event.x(),self.width())
+        self.setToolTip(u'%s' % datetime.timedelta(seconds=int(timevalue/1000)))
+        QtGui.QSlider.mouseMoveEvent(self, event)
+
+    def mousePressEvent(self, event):
+        """
+        Mouse Press event no new functionality
+        """
+        QtGui.QSlider.mousePressEvent(self, event)
+
+    def mouseReleaseEvent(self, event):
+        """
+        Set the slider position when the mouse is clicked and released on the slider.
+        """
+        self.setValue(QtGui.QStyle.sliderValueFromPosition(self.minimum(),self.maximum(),event.x(),self.width()))
+        QtGui.QSlider.mouseReleaseEvent(self, event)
+
+
 class MediaController(object):
     """
     The implementation of the Media Controller. The Media Controller adds an own
@@ -69,8 +99,8 @@
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackPlay'), self.media_play_msg)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackPause'), self.media_pause_msg)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'playbackStop'), self.media_stop_msg)
-        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'seekSlider'), self.media_seek)
-        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'volumeSlider'), self.media_volume)
+        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'seekSlider'), self.media_seek_msg)
+        QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'volumeSlider'), self.media_volume_msg)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_hide'), self.media_hide)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_blank'), self.media_blank)
         QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'media_unblank'), self.media_unblank)
@@ -241,9 +271,10 @@
             icon=u':/slides/media_playback_stop.png',
             tooltip=translate('OpenLP.SlideController', 'Stop playing media.'), triggers=controller.sendToPlugins)
         # Build the seekSlider.
-        controller.seekSlider = QtGui.QSlider(QtCore.Qt.Horizontal)
+        controller.seekSlider = MediaSlider(QtCore.Qt.Horizontal, self, controller)
         controller.seekSlider.setMaximum(1000)
-        controller.seekSlider.setTracking(False)
+        controller.seekSlider.setTracking(True)
+        controller.seekSlider.setMouseTracking(True)
         controller.seekSlider.setToolTip(translate('OpenLP.SlideController', 'Video position.'))
         controller.seekSlider.setGeometry(QtCore.QRect(90, 260, 221, 24))
         controller.seekSlider.setObjectName(u'seekSlider')
@@ -344,12 +375,8 @@
         # stop running videos
         self.media_reset(controller)
         controller.media_info = MediaInfo()
-        if videoBehindText:
-            controller.media_info.volume = 0
-            controller.media_info.is_background = True
-        else:
-            controller.media_info.volume = controller.volumeSlider.value()
-            controller.media_info.is_background = False
+        controller.media_info.volume = controller.volumeSlider.value()
+        controller.media_info.is_background = videoBehindText
         controller.media_info.file_info = QtCore.QFileInfo(serviceItem.get_frame_path())
         display = self._define_display(controller)
         if controller.isLive:
@@ -361,7 +388,7 @@
                 controller.media_info.start_time = 0
                 controller.media_info.end_time = 0
             else:
-                controller.media_info.start_time = display.serviceItem.start_time
+                controller.media_info.start_time = serviceItem.start_time
                 controller.media_info.end_time = serviceItem.end_time
         elif controller.previewDisplay:
             isValid = self._check_file_type(controller, display, serviceItem)
@@ -483,9 +510,17 @@
             The controller to be played
         """
         log.debug(u'media_play')
+        controller.seekSlider.blockSignals(True)
+        controller.volumeSlider.blockSignals(True)
         display = self._define_display(controller)
         if not self.currentMediaPlayer[controller.controllerType].play(display):
+            controller.seekSlider.blockSignals(False)
+            controller.volumeSlider.blockSignals(False)
             return False
+        if controller.media_info.is_background:
+            self.media_volume(controller, 0)
+        else:
+            self.media_volume(controller, controller.media_info.volume)
         if status:
             display.frame.evaluateJavaScript(u'show_blank("desktop");')
             self.currentMediaPlayer[controller.controllerType].set_visible(display, True)
@@ -503,6 +538,8 @@
         # Start Timer for ui updates
         if not self.timer.isActive():
             self.timer.start()
+        controller.seekSlider.blockSignals(False)
+        controller.volumeSlider.blockSignals(False)
         return True
 
     def media_pause_msg(self, msg):
@@ -557,7 +594,7 @@
             controller.mediabar.actions[u'playbackStop'].setVisible(False)
             controller.mediabar.actions[u'playbackPause'].setVisible(False)
 
-    def media_volume(self, msg):
+    def media_volume_msg(self, msg):
         """
         Changes the volume of a running video
 
@@ -566,11 +603,21 @@
         """
         controller = msg[0]
         vol = msg[1][0]
-        log.debug(u'media_volume %d' % vol)
+        self.media_volume(controller, vol)
+
+    def media_volume(self, controller, volume):
+        """
+        Changes the volume of a running video
+
+        ``msg``
+            First element is the controller which should be used
+        """
+        log.debug(u'media_volume %d' % volume)
         display = self._define_display(controller)
-        self.currentMediaPlayer[controller.controllerType].volume(display, vol)
+        self.currentMediaPlayer[controller.controllerType].volume(display, volume)
+        controller.volumeSlider.setValue(volume)
 
-    def media_seek(self, msg):
+    def media_seek_msg(self, msg):
         """
         Responds to the request to change the seek Slider of a loaded video
 
@@ -581,6 +628,17 @@
         log.debug(u'media_seek')
         controller = msg[0]
         seekVal = msg[1][0]
+        self.media_seek(controller, seekVal)
+
+    def media_seek(self, controller, seekVal):
+        """
+        Responds to the request to change the seek Slider of a loaded video
+
+        ``msg``
+            First element is the controller which should be used
+            Second element is a list with the seek Value as first element
+        """
+        log.debug(u'media_seek')
         display = self._define_display(controller)
         self.currentMediaPlayer[controller.controllerType].seek(display, seekVal)
 

=== modified file 'openlp/core/ui/media/phononplayer.py'
--- openlp/core/ui/media/phononplayer.py	2012-12-27 16:27:59 +0000
+++ openlp/core/ui/media/phononplayer.py	2012-12-30 19:59:23 +0000
@@ -31,7 +31,7 @@
 import mimetypes
 from datetime import datetime
 
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtGui
 from PyQt4.phonon import Phonon
 
 from openlp.core.lib import Receiver, translate, Settings

=== modified file 'openlp/core/ui/media/vlcplayer.py'
--- openlp/core/ui/media/vlcplayer.py	2012-12-27 16:27:59 +0000
+++ openlp/core/ui/media/vlcplayer.py	2012-12-30 19:59:23 +0000
@@ -33,7 +33,7 @@
 import os
 import sys
 
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtGui
 
 from openlp.core.lib import Receiver, translate, Settings
 from openlp.core.ui.media import MediaState
@@ -188,6 +188,7 @@
         display.vlcMediaPlayer.play()
         if not self.media_state_wait(display, vlc.State.Playing):
             return False
+        self.volume(display, controller.media_info.volume)
         if start_time > 0:
             self.seek(display, controller.media_info.start_time * 1000)
         controller.media_info.length = int(display.vlcMediaPlayer.get_media().get_duration() / 1000)

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2012-12-28 22:06:43 +0000
+++ openlp/core/ui/servicemanager.py	2012-12-30 19:59:23 +0000
@@ -114,6 +114,7 @@
         # is a new service and has not been saved
         self._modified = False
         self._fileName = u''
+        self.service_has_all_original_files = True
         self.serviceNoteForm = ServiceNoteForm(self.mainwindow)
         self.serviceItemEditForm = ServiceItemEditForm(self.mainwindow)
         self.startTimeForm = StartTimeForm(self.mainwindow)
@@ -455,7 +456,7 @@
         for item in list(self.serviceItems):
             self.mainwindow.incrementProgressBar()
             item[u'service_item'].remove_invalid_frames(missing_list)
-            if not item[u'service_item'].validate():
+            if item[u'service_item'].missing_frames():
                 self.serviceItems.remove(item)
             else:
                 service_item = item[u'service_item'].get_service_repr(self._saveLite)
@@ -690,7 +691,7 @@
                         serviceItem.set_from_service(item)
                     else:
                         serviceItem.set_from_service(item, self.servicePath)
-                    self.validateItem(serviceItem)
+                    serviceItem.validate_item(self.suffixes)
                     self.load_item_uuid = 0
                     if serviceItem.is_capable(ItemCapabilities.OnLoadUpdate):
                         Receiver.send_message(u'%s_service_load' % serviceItem.name.lower(), serviceItem)
@@ -1023,9 +1024,12 @@
         """
         # Correct order of items in array
         count = 1
+        self.service_has_all_original_files = True
         for item in self.serviceItems:
             item[u'order'] = count
             count += 1
+            if not item[u'service_item'].has_original_files:
+                self.service_has_all_original_files = False
         # Repaint the screen
         self.serviceManagerList.clear()
         for itemcount, item in enumerate(self.serviceItems):
@@ -1084,18 +1088,6 @@
                         self.serviceManagerList.setCurrentItem(treewidgetitem)
             treewidgetitem.setExpanded(item[u'expanded'])
 
-    def validateItem(self, serviceItem):
-        """
-        Validates the service item and if the suffix matches an accepted
-        one it allows the item to be displayed.
-        """
-        #@todo check file items exist
-        if serviceItem.is_command():
-            type = serviceItem._raw_frames[0][u'title'].split(u'.')[-1]
-            if type.lower() not in self.suffixes:
-                serviceItem.is_valid = False
-            #@todo check file items exist
-
     def cleanUp(self):
         """
         Empties the servicePath of temporary files on system exit.
@@ -1135,7 +1127,7 @@
         Receiver.send_message(u'cursor_busy')
         log.debug(u'regenerateServiceItems')
         # force reset of renderer as theme data has changed
-        self.mainwindow.renderer.themedata = None
+        self.service_has_all_original_files = True
         if self.serviceItems:
             for item in self.serviceItems:
                 item[u'selected'] = False

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2012-12-27 16:27:59 +0000
+++ openlp/core/ui/slidecontroller.py	2012-12-30 19:59:23 +0000
@@ -933,8 +933,7 @@
             else:
                 if not self.serviceItem.is_command():
                     Receiver.send_message(u'live_display_show')
-                Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(),
-                    [self.serviceItem, self.isLive])
+                Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive])
         else:
             if hide_mode:
                 Receiver.send_message(u'live_display_hide', hide_mode)
@@ -949,13 +948,11 @@
         if self.serviceItem is not None:
             if hide:
                 Receiver.send_message(u'live_display_hide', HideMode.Screen)
-                Receiver.send_message(u'%s_hide' % self.serviceItem.name.lower(),
-                    [self.serviceItem, self.isLive])
+                Receiver.send_message(u'%s_hide' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive])
             else:
                 if not self.serviceItem.is_command():
                     Receiver.send_message(u'live_display_show')
-                Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(),
-                    [self.serviceItem, self.isLive])
+                Receiver.send_message(u'%s_unblank' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive])
         else:
             if hide:
                 Receiver.send_message(u'live_display_hide', HideMode.Screen)

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2012-12-28 22:06:43 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2012-12-30 19:59:23 +0000
@@ -33,11 +33,11 @@
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import MediaManagerItem, build_icon, ItemCapabilities, SettingsManager, translate, \
-    check_item_selected, Receiver, MediaType, ServiceItem, build_html, ServiceItemContext, Settings
+    check_item_selected, Receiver, MediaType, ServiceItem, ServiceItemContext, Settings, check_directory_exists
 from openlp.core.lib.ui import UiStrings, critical_error_message_box, create_horizontal_adjusting_combo_box
 from openlp.core.ui import DisplayController, Display, DisplayControllerType
 from openlp.core.ui.media import get_media_players, set_media_players
-from openlp.core.utils import locale_compare
+from openlp.core.utils import AppLocation, locale_compare
 
 log = logging.getLogger(__name__)
 
@@ -130,8 +130,7 @@
         """
         Called to reset the Live background with the media selected,
         """
-        self.plugin.liveController.mediaController.media_reset(
-            self.plugin.liveController)
+        self.plugin.liveController.mediaController.media_reset(self.plugin.liveController)
         self.resetAction.setVisible(False)
 
     def videobackgroundReplaced(self):
@@ -145,8 +144,7 @@
         Called to replace Live background with the media selected.
         """
         if check_item_selected(self.listView,
-            translate('MediaPlugin.MediaItem',
-            'You must select a media file to replace the background with.')):
+                translate('MediaPlugin.MediaItem', 'You must select a media file to replace the background with.')):
             item = self.listView.currentItem()
             filename = item.data(QtCore.Qt.UserRole)
             if os.path.exists(filename):
@@ -166,8 +164,8 @@
                     translate('MediaPlugin.MediaItem',
                     'There was a problem replacing your background, the media file "%s" no longer exists.') % filename)
 
-    def generateSlideData(self, service_item, item=None, xmlVersion=False,
-        remote=False, context=ServiceItemContext.Live):
+    def generateSlideData(self, service_item, item=None, xmlVersion=False, remote=False,
+            context=ServiceItemContext.Live):
         if item is None:
             item = self.listView.currentItem()
             if item is None:
@@ -201,6 +199,8 @@
     def initialise(self):
         self.listView.clear()
         self.listView.setIconSize(QtCore.QSize(88, 50))
+        self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settingsSection), u'thumbnails')
+        check_directory_exists(self.servicePath)
         self.loadList(SettingsManager.load_list(self.settingsSection, u'media'))
         self.populateDisplayTypes()
 
@@ -247,14 +247,13 @@
         """
         Remove a media item from the list.
         """
-        if check_item_selected(self.listView, translate('MediaPlugin.MediaItem',
-                'You must select a media file to delete.')):
+        if check_item_selected(self.listView,
+                translate('MediaPlugin.MediaItem', 'You must select a media file to delete.')):
             row_list = [item.row() for item in self.listView.selectedIndexes()]
             row_list.sort(reverse=True)
             for row in row_list:
                 self.listView.takeItem(row)
-            SettingsManager.set_list(self.settingsSection,
-                u'media', self.getFileList())
+            SettingsManager.set_list(self.settingsSection, u'media', self.getFileList())
 
     def loadList(self, media):
         # Sort the media by its filename considering language specific

=== added directory 'tests/functional/openlp_core_lib/resources'
=== added file 'tests/functional/openlp_core_lib/resources/church.jpg'
Binary files tests/functional/openlp_core_lib/resources/church.jpg	1970-01-01 00:00:00 +0000 and tests/functional/openlp_core_lib/resources/church.jpg	2012-12-30 19:59:23 +0000 differ
=== added file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py	2012-12-30 19:59:23 +0000
@@ -0,0 +1,99 @@
+"""
+    Package to test the openlp.core.lib package.
+"""
+from unittest import TestCase
+from mock import MagicMock
+from openlp.core.lib import ServiceItem
+
+VERSE = u'The Lord said to {r}Noah{/r}: \n'\
+        'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
+        'The Lord said to {g}Noah{/g}:\n'\
+        'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n'\
+        'Get those children out of the muddy, muddy \n'\
+        '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
+        'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
+FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
+
+class TestServiceItem(TestCase):
+
+    def serviceitem_basic_test(self):
+        """
+        Test the Service Item
+        """
+        #GIVEN: A new service item
+
+        # WHEN:A service item is created (without a plugin)
+        service_item = ServiceItem(None)
+
+        # THEN: We should get back a valid service item
+        assert service_item.is_valid is True, u'A valid Service Item'
+        assert service_item.missing_frames() is True, u'No frames loaded yet'
+
+    def serviceitem_add_text_test(self):
+        """
+        Test the Service Item
+        """
+        #GIVEN: A new service item
+        service_item = ServiceItem(None)
+
+        # WHEN: adding text to a service item
+        service_item.add_from_text(VERSE)
+        service_item.raw_footer = FOOTER
+
+        # THEN: We should get back a valid service item
+        assert service_item.is_valid is True, u'A valid Service Item'
+        assert service_item.missing_frames() is False, u'frames loaded '
+
+        #GIVEN: A service item with text
+        mocked_renderer =  MagicMock()
+        mocked_renderer.format_slide.return_value = [VERSE]
+        service_item.renderer = mocked_renderer
+
+        #WHEN: Render called
+        assert len(service_item._display_frames) is 0, u'A blank Service Item'
+        service_item.render(True)
+
+        #THEN: We should should have a page of output.
+        assert len(service_item._display_frames) is 1, u'A valid rendered Service Item has display frames'
+        assert service_item.get_rendered_frame(0) == VERSE.split(u'\n')[0], u'A valid render'
+
+    def serviceitem_add_image_test_single(self):
+        """
+        Test the Service Item
+        """
+        #GIVEN: A new service item and a mocked renderer
+        service_item = ServiceItem(None)
+        service_item.name = u'test'
+        mocked_renderer =  MagicMock()
+        service_item.renderer = mocked_renderer
+
+        # WHEN: adding image to a service item
+        service_item.add_from_image(u'resources/church.jpg', u'Image Title')
+
+        # THEN: We should get back a valid service item
+        assert service_item.is_valid is True, u'A valid Service Item'
+        assert service_item.missing_frames() is False, u'frames loaded '
+        assert len(service_item._display_frames) is 0, u'A blank Service Item'
+
+        #THEN: We should should have a page of output.
+        assert len(service_item._raw_frames) is 1, u'A valid rendered Service Item has display frames'
+        assert service_item.get_rendered_frame(0) == u'resources/church.jpg'
+
+        # WHEN: adding a second image to a service item
+        service_item.add_from_image(u'resources/church.jpg', u'Image1 Title')
+
+        #THEN: We should should have a page of output.
+        assert len(service_item._raw_frames) is 2, u'A valid rendered Service Item has display frames'
+        assert service_item.get_rendered_frame(0) == u'resources/church.jpg'
+        assert service_item.get_rendered_frame(0) == service_item.get_rendered_frame(1)
+
+        #When requesting a saved service item
+        service = service_item.get_service_repr(True)
+
+        #THEN: We should should have two parts of the service.
+        assert len(service) is 2, u'A saved service has two parts'
+        assert service[u'header'][u'name']  == u'test' , u'A test plugin'
+        assert service[u'data'][0][u'title'] == u'Image Title' , u'The first title name '
+        assert service[u'data'][0][u'path'] == u'resources/church.jpg' , u'The first image name'
+        assert service[u'data'][0][u'title'] != service[u'data'][1][u'title'], u'The titles should not match'
+        assert service[u'data'][0][u'path'] == service[u'data'][1][u'path'], u'The files should match'
\ No newline at end of file

=== modified file 'tests/test_app.py'
--- tests/test_app.py	2012-06-22 14:14:53 +0000
+++ tests/test_app.py	2012-12-30 19:59:23 +0000
@@ -34,4 +34,4 @@
 def test_start_app(openlpapp):
     assert type(openlpapp) == OpenLP
     assert type(openlpapp.mainWindow) == MainWindow
-    assert unicode(openlpapp.mainWindow.windowTitle()) == u'OpenLP 2.0'
+    assert unicode(openlpapp.mainWindow.windowTitle()) == u'OpenLP 2.1'