← Back to team overview

openlp-core team mailing list archive

Re: [Merge] lp:~tomasgroth/openlp/better-remote into lp:openlp

 

Review: Needs Fixing

See inline comments.

Diff comments:

> === modified file 'openlp/core/lib/__init__.py'
> --- openlp/core/lib/__init__.py	2014-05-01 17:49:43 +0000
> +++ openlp/core/lib/__init__.py	2014-09-21 19:28:31 +0000
> @@ -145,11 +145,13 @@
>      return button_icon
>  
>  
> -def image_to_byte(image):
> +def image_to_byte(image, base_64=True):
>      """
>      Resize an image to fit on the current screen for the web and returns it as a byte stream.
>  
>      :param image: The image to converted.
> +    :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
> +        To preserve original intention, this defaults to True
>      """
>      log.debug('image_to_byte - start')
>      byte_array = QtCore.QByteArray()
> @@ -158,6 +160,8 @@
>      buffie.open(QtCore.QIODevice.WriteOnly)
>      image.save(buffie, "PNG")
>      log.debug('image_to_byte - end')
> +    if not base_64:
> +        return byte_array
>      # convert to base64 encoding so does not get missed!
>      return bytes(byte_array.toBase64()).decode('utf-8')
>  
> 
> === modified file 'openlp/core/lib/imagemanager.py'
> --- openlp/core/lib/imagemanager.py	2014-03-20 19:10:31 +0000
> +++ openlp/core/lib/imagemanager.py	2014-09-21 19:28:31 +0000
> @@ -106,7 +106,7 @@
>      """
>      secondary_priority = 0
>  
> -    def __init__(self, path, source, background):
> +    def __init__(self, path, source, background, width=-1, height=-1):
>          """
>          Create an image for the :class:`ImageManager`'s cache.
>  
> @@ -115,7 +115,8 @@
>              :class:`~openlp.core.lib.ImageSource` class.
>          :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
>              ratio does not match with the display ratio.
> -
> +        :param width: The width of the image, defaults to -1 meaning that the screen width will be used.
> +        :param height: The height of the image, defaults to -1 meaning that the screen height will be used.
>          """
>          self.path = path
>          self.image = None
> @@ -124,6 +125,8 @@
>          self.source = source
>          self.background = background
>          self.timestamp = 0
> +        self.width = width
> +        self.height = height
>          # FIXME: We assume that the path exist. The caller has to take care that it exists!
>          if os.path.exists(path):
>              self.timestamp = os.stat(path).st_mtime
> @@ -210,13 +213,13 @@
>                  image.background = background
>                  self._reset_image(image)
>  
> -    def update_image_border(self, path, source, background):
> +    def update_image_border(self, path, source, background, width=-1, height=-1):
>          """
>          Border has changed so update the image affected.
>          """
>          log.debug('update_image_border')
>          # Mark the image as dirty for a rebuild by setting the image and byte stream to None.
> -        image = self._cache[(path, source)]
> +        image = self._cache[(path, source, width, height)]
>          if image.source == source:
>              image.background = background
>              self._reset_image(image)
> @@ -237,12 +240,12 @@
>          if not self.image_thread.isRunning():
>              self.image_thread.start()
>  
> -    def get_image(self, path, source):
> +    def get_image(self, path, source, width=-1, height=-1):
>          """
>          Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
>          """
>          log.debug('getImage %s' % path)
> -        image = self._cache[(path, source)]
> +        image = self._cache[(path, source, width, height)]
>          if image.image is None:
>              self._conversion_queue.modify_priority(image, Priority.High)
>              # make sure we are running and if not give it a kick
> @@ -257,12 +260,12 @@
>              self._conversion_queue.modify_priority(image, Priority.Low)
>          return image.image
>  
> -    def get_image_bytes(self, path, source):
> +    def get_image_bytes(self, path, source, width=-1, height=-1):
>          """
>          Returns the byte string for an image. If not present wait for the background thread to process it.
>          """
>          log.debug('get_image_bytes %s' % path)
> -        image = self._cache[(path, source)]
> +        image = self._cache[(path, source, width, height)]
>          if image.image_bytes is None:
>              self._conversion_queue.modify_priority(image, Priority.Urgent)
>              # make sure we are running and if not give it a kick
> @@ -272,14 +275,14 @@
>                  time.sleep(0.1)
>          return image.image_bytes
>  
> -    def add_image(self, path, source, background):
> +    def add_image(self, path, source, background, width=-1, height=-1):
>          """
>          Add image to cache if it is not already there.
>          """
>          log.debug('add_image %s' % path)
> -        if not (path, source) in self._cache:
> -            image = Image(path, source, background)
> -            self._cache[(path, source)] = image
> +        if not (path, source, width, height) in self._cache:
> +            image = Image(path, source, background, width, height)
> +            self._cache[(path, source, width, height)] = image
>              self._conversion_queue.put((image.priority, image.secondary_priority, image))
>          # Check if the there are any images with the same path and check if the timestamp has changed.
>          for image in list(self._cache.values()):
> @@ -308,7 +311,10 @@
>          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, image.background)
> +            # Let's see if the image was requested with specific dimensions
> +            width = self.width if image.width == -1 else image.width
> +            height = self.height if image.height == -1 else image.height
> +            image.image = resize_image(image.path, width, height, image.background)
>              # Set the priority to Lowest and stop here as we need to process more important images first.
>              if image.priority == Priority.Normal:
>                  self._conversion_queue.modify_priority(image, Priority.Lowest)
> 
> === modified file 'openlp/core/lib/serviceitem.py'
> --- openlp/core/lib/serviceitem.py	2014-06-17 07:27:12 +0000
> +++ openlp/core/lib/serviceitem.py	2014-09-21 19:28:31 +0000
> @@ -39,8 +39,8 @@
>  
>  from PyQt4 import QtGui
>  
> -from openlp.core.common import RegistryProperties, Settings, translate
> -from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
> +from openlp.core.common import RegistryProperties, Settings, translate, AppLocation
> +from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
>  
>  log = logging.getLogger(__name__)
>  
> @@ -112,7 +112,17 @@
>              The capability to edit the title of the item
>  
>      ``IsOptical``
> -            .Determines is the service_item is based on an optical device
> +            Determines is the service_item is based on an optical device
> +
> +    ``HasDisplayTitle``
> +            The item contains 'displaytitle' on every frame which should be
> +            preferred over 'title' when displaying the item
> +
> +    ``HasNotes``
> +            The item contains 'notes'
> +
> +    ``HasThumbnails``
> +            The item has related thumbnails available
>  
>      """
>      CanPreview = 1
> @@ -133,6 +143,9 @@
>      CanAutoStartForLive = 16
>      CanEditTitle = 17
>      IsOptical = 18
> +    HasDisplayTitle = 19
> +    HasNotes = 20
> +    HasThumbnails = 21
>  
>  
>  class ServiceItem(RegistryProperties):
> @@ -272,18 +285,23 @@
>              self.raw_footer = []
>          self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
>  
> -    def add_from_image(self, path, title, background=None):
> +    def add_from_image(self, path, title, background=None, thumbnail=None):
>          """
>          Add an image slide to the service item.
>  
>          :param path: The directory in which the image file is located.
>          :param title: A title for the slide in the service item.
>          :param background:
> +        :param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
>          """
>          if background:
>              self.image_border = background
>          self.service_item_type = ServiceItemType.Image
> -        self._raw_frames.append({'title': title, 'path': path})
> +        # If no thumbnail was given we create one
> +        if not thumbnail:

Do we need this as it is a leakage for plugin code into the service item.

> +            thumbnail = os.path.join(AppLocation.get_section_data_path('images'), 'thumbnails', os.path.split(path)[1])
> +            create_thumb(path, thumbnail, False)
> +        self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
>          self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
>          self._new_item()
>  
> @@ -301,16 +319,22 @@
>          self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
>          self._new_item()
>  
> -    def add_from_command(self, path, file_name, image):
> +    def add_from_command(self, path, file_name, image, display_title=None, notes=None):
>          """
>          Add a slide from a command.
>  
>          :param path: The title of the slide in the service item.
>          :param file_name: The title of the slide in the service item.
>          :param image: The command of/for the slide.
> +        :param display_title: Title to show in gui/webinterface, optional.
> +        :param notes: Notes to show in the webinteface, optional.
>          """
>          self.service_item_type = ServiceItemType.Command
> -        self._raw_frames.append({'title': file_name, 'image': image, 'path': path})
> +        # If the item should have a display title but this frame doesn't have one, we make one up
> +        if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
> +            display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1)
> +        self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
> +                                 'display_title': display_title, 'notes': notes})
>          self._new_item()
>  
>      def get_service_repr(self, lite_save):
> @@ -354,7 +378,8 @@
>                  service_data = [slide['title'] for slide in self._raw_frames]
>          elif self.service_item_type == ServiceItemType.Command:
>              for slide in self._raw_frames:
> -                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']})
> +                service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
> +                                     'display_title': slide['display_title'], 'notes': slide['notes']})
>          return {'header': service_header, 'data': service_data}
>  
>      def set_from_service(self, service_item, path=None):
> @@ -425,7 +450,8 @@
>                      self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
>                  elif path:
>                      self.has_original_files = False
> -                    self.add_from_command(path, text_image['title'], text_image['image'])
> +                    self.add_from_command(path, text_image['title'], text_image['image'],
> +                                          text_image.get('display_title', ''), text_image.get('notes', ''))
>                  else:
>                      self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
>          self._new_item()
> 
> === modified file 'openlp/core/ui/listpreviewwidget.py'
> --- openlp/core/ui/listpreviewwidget.py	2014-05-19 19:19:05 +0000
> +++ openlp/core/ui/listpreviewwidget.py	2014-09-21 19:28:31 +0000
> @@ -94,8 +94,8 @@
>          Displays the given slide.
>          """
>          self.service_item = service_item
> +        self.setRowCount(0)
>          self.clear()
> -        self.setRowCount(0)
>          self.setColumnWidth(0, width)
>          row = 0
>          text = []
> 
> === modified file 'openlp/core/ui/servicemanager.py'
> --- openlp/core/ui/servicemanager.py	2014-08-23 20:42:10 +0000
> +++ openlp/core/ui/servicemanager.py	2014-09-21 19:28:31 +0000
> @@ -1281,7 +1281,11 @@
>              # Add the children to their parent tree_widget_item.
>              for count, frame in enumerate(service_item_from_item.get_frames()):
>                  child = QtGui.QTreeWidgetItem(tree_widget_item)
> -                text = frame['title'].replace('\n', ' ')
> +                # prefer to use a display_title
> +                if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
> +                    text = frame['display_title'].replace('\n', ' ')
> +                else:
> +                    text = frame['title'].replace('\n', ' ')
>                  child.setText(0, text[:40])
>                  child.setData(0, QtCore.Qt.UserRole, count)
>                  if service_item == item_count:
> 
> === modified file 'openlp/core/ui/slidecontroller.py'
> --- openlp/core/ui/slidecontroller.py	2014-07-12 23:47:53 +0000
> +++ openlp/core/ui/slidecontroller.py	2014-09-21 19:28:31 +0000
> @@ -873,6 +873,7 @@
>          if self.service_item.is_command():
>              Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
>              self.update_preview()
> +            self.selected_row = index
>          else:
>              self.preview_widget.change_slide(index)
>              self.slide_selected()
> @@ -1042,8 +1043,8 @@
>                          self.display.image(to_display)
>                      # reset the store used to display first image
>                      self.service_item.bg_image_bytes = None
> -            self.update_preview()
>              self.selected_row = row
> +            self.update_preview()
>              self.preview_widget.change_slide(row)
>          self.display.setFocus()
>  
> @@ -1055,6 +1056,7 @@
>          """
>          self.preview_widget.change_slide(row)
>          self.update_preview()
> +        self.selected_row = row
>  
>      def update_preview(self):
>          """
> 
> === modified file 'openlp/plugins/bibles/lib/manager.py'
> --- openlp/plugins/bibles/lib/manager.py	2014-05-06 19:53:08 +0000
> +++ openlp/plugins/bibles/lib/manager.py	2014-09-21 19:28:31 +0000
> @@ -85,7 +85,7 @@
>              BibleFormat.CSV,
>              BibleFormat.OpenSong,
>              BibleFormat.WebDownload,
> -            BibleFormar.Zefania,
> +            BibleFormat.Zefania,
>          ]
>  
>  
> 
> === modified file 'openlp/plugins/images/lib/mediaitem.py'
> --- openlp/plugins/images/lib/mediaitem.py	2014-04-24 02:57:02 +0000
> +++ openlp/plugins/images/lib/mediaitem.py	2014-09-21 19:28:31 +0000
> @@ -551,6 +551,7 @@
>          service_item.add_capability(ItemCapabilities.CanLoop)
>          service_item.add_capability(ItemCapabilities.CanAppend)
>          service_item.add_capability(ItemCapabilities.CanEditTitle)
> +        service_item.add_capability(ItemCapabilities.HasThumbnails)
>          # force a nonexistent theme
>          service_item.theme = -1
>          missing_items_file_names = []
> @@ -589,7 +590,7 @@
>          # Continue with the existing images.
>          for filename in images_file_names:
>              name = os.path.split(filename)[1]
> -            service_item.add_from_image(filename, name, background)
> +            service_item.add_from_image(filename, name, background, os.path.join(self.service_path, name))
>          return True
>  
>      def check_group_exists(self, new_group):
> 
> === modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
> --- openlp/plugins/presentations/lib/impresscontroller.py	2014-08-27 23:18:06 +0000
> +++ openlp/plugins/presentations/lib/impresscontroller.py	2014-09-21 19:28:31 +0000
> @@ -65,7 +65,7 @@
>  
>  from openlp.core.lib import ScreenList
>  from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
> -from .presentationcontroller import PresentationController, PresentationDocument
> +from .presentationcontroller import PresentationController, PresentationDocument, TextType
>  
>  
>  log = logging.getLogger(__name__)
> @@ -257,6 +257,7 @@
>          self.presentation.Display = ScreenList().current['number'] + 1
>          self.control = None
>          self.create_thumbnails()
> +        self.create_titles_and_notes()
>          return True
>  
>      def create_thumbnails(self):
> @@ -450,22 +451,46 @@
>  
>          :param slide_no: The slide the notes are required for, starting at 1
>          """
> -        return self.__get_text_from_page(slide_no, True)
> +        return self.__get_text_from_page(slide_no, TextType.Notes)
>  
> -    def __get_text_from_page(self, slide_no, notes=False):
> +    def __get_text_from_page(self, slide_no, text_type=TextType.SlideText):
>          """
>          Return any text extracted from the presentation page.
>  
>          :param slide_no: The slide the notes are required for, starting at 1
>          :param notes: A boolean. If set the method searches the notes of the slide.
> +        :param text_type: A TextType. Enumeration of the types of supported text.
>          """
>          text = ''
> +        if TextType.Title <= text_type <= TextType.Notes:
> +            pages = self.document.getDrawPages()
> +            if 0 < slide_no <= pages.getCount():
> +                page = pages.getByIndex(slide_no - 1)
> +                if text_type == TextType.Notes:
> +                    page = page.getNotesPage()
> +                for index in range(page.getCount()):
> +                    shape = page.getByIndex(index)
> +                    shape_type = shape.getShapeType()
> +                    if shape.supportsService("com.sun.star.drawing.Text"):
> +                        # if they requested title, make sure it is the title
> +                        if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape":
> +                            text += shape.getString() + '\n'
> +        return text
> +
> +    def create_titles_and_notes(self):
> +        """
> +        Writes the list of titles (one per slide)

Wrapping! could make the lines longer!

> +        to 'titles.txt'
> +        and the notes to 'slideNotes[x].txt'
> +        in the thumbnails directory
> +        """
> +        titles = []
> +        notes = []
>          pages = self.document.getDrawPages()
> -        page = pages.getByIndex(slide_no - 1)
> -        if notes:
> -            page = page.getNotesPage()
> -        for index in range(page.getCount()):
> -            shape = page.getByIndex(index)
> -            if shape.supportsService("com.sun.star.drawing.Text"):
> -                text += shape.getString() + '\n'
> -        return text
> +        for slide_no in range(1, pages.getCount() + 1):
> +            titles.append(self.__get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
> +            note = self.__get_text_from_page(slide_no, TextType.Notes)
> +            if len(note) == 0:
> +                note = ' '
> +            notes.append(note)
> +        self.save_titles_and_notes(titles, notes)
> 
> === modified file 'openlp/plugins/presentations/lib/mediaitem.py'
> --- openlp/plugins/presentations/lib/mediaitem.py	2014-06-05 16:25:37 +0000
> +++ openlp/plugins/presentations/lib/mediaitem.py	2014-09-21 19:28:31 +0000
> @@ -288,13 +288,14 @@
>                              os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
>                          doc.load_presentation()
>                      i = 1
> -                    image_file = 'mainslide%03d.png' % i
> -                    image = os.path.join(doc.get_temp_folder(), image_file)
> +                    image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
> +                    thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
>                      while os.path.isfile(image):
> -                        service_item.add_from_image(image, name)
> +                        service_item.add_from_image(image, name, thumbnail=thumbnail)
>                          i += 1
> -                        image_file = 'mainslide%03d.png' % i
> -                        image = os.path.join(doc.get_temp_folder(), image_file)
> +                        image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
> +                        thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
> +                    service_item.add_capability(ItemCapabilities.HasThumbnails)
>                      doc.close_presentation()
>                      return True
>                  else:
> @@ -323,8 +324,21 @@
>                      i = 1
>                      img = doc.get_thumbnail_path(i, True)
>                      if img:
> +                        # Get titles and notes
> +                        titles, notes = doc.get_titles_and_notes()
> +                        service_item.add_capability(ItemCapabilities.HasDisplayTitle)
> +                        if notes.count('') != len(notes):
> +                            service_item.add_capability(ItemCapabilities.HasNotes)
> +                        service_item.add_capability(ItemCapabilities.HasThumbnails)
>                          while img:
> -                            service_item.add_from_command(path, name, img)
> +                            # Use title and note if available
> +                            title = ''
> +                            if titles and len(titles) >= i:
> +                                title = titles[i - 1]
> +                            note = ''
> +                            if notes and len(notes) >= i:
> +                                note = notes[i - 1]
> +                            service_item.add_from_command(path, name, img, title, note)
>                              i += 1
>                              img = doc.get_thumbnail_path(i, True)
>                          doc.close_presentation()
> 
> === modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
> --- openlp/plugins/presentations/lib/powerpointcontroller.py	2014-08-27 23:18:06 +0000
> +++ openlp/plugins/presentations/lib/powerpointcontroller.py	2014-09-21 19:28:31 +0000
> @@ -27,7 +27,7 @@
>  # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
>  ###############################################################################
>  """
> -This modul is for controlling powerpiont. PPT API documentation:
> +This module is for controlling powerpoint. PPT API documentation:
>  `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
>  """
>  import os
> @@ -37,16 +37,17 @@
>  
>  if is_win():
>      from win32com.client import Dispatch
> +    import win32com
>      import winreg
>      import win32ui
>      import pywintypes
>  
>  from openlp.core.lib import ScreenList
> +from openlp.core.common import Registry
>  from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
>  from openlp.core.common import trace_error_handler
>  from .presentationcontroller import PresentationController, PresentationDocument
>  
> -
>  log = logging.getLogger(__name__)
>  
>  
> @@ -136,6 +137,7 @@
>              self.controller.process.Presentations.Open(self.file_path, False, False, True)
>              self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
>              self.create_thumbnails()
> +            self.create_titles_and_notes()
>              # Powerpoint 2013 pops up when loading a file, so we minimize it again
>              if self.presentation.Application.Version == u'15.0':
>                  try:
> @@ -392,6 +394,28 @@
>          """
>          return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
>  
> +    def create_titles_and_notes(self):
> +        """
> +        Writes the list of titles (one per slide)
> +        to 'titles.txt'
> +        and the notes to 'slideNotes[x].txt'
> +        in the thumbnails directory
> +        """
> +        titles = []
> +        notes = []
> +        for slide in self.presentation.Slides:
> +            try:
> +                text = slide.Shapes.Title.TextFrame.TextRange.Text
> +            except Exception as e:
> +                log.exception(e)
> +                text = ''
> +            titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n')
> +            note = _get_text_from_shapes(slide.NotesPage.Shapes)
> +            if len(note) == 0:
> +                note = ' '
> +            notes.append(note)
> +        self.save_titles_and_notes(titles, notes)
> +
>      def show_error_msg(self):
>          """
>          Stop presentation and display an error message.
> @@ -410,8 +434,8 @@
>      :param shapes: A set of shapes to search for text.
>      """
>      text = ''
> -    for index in range(shapes.Count):
> -        shape = shapes(index + 1)
> -        if shape.HasTextFrame:
> -            text += shape.TextFrame.TextRange.Text + '\n'
> +    for shape in shapes:
> +        if shape.PlaceholderFormat.Type == 2:  # 2 from is enum PpPlaceholderType.ppPlaceholderBody
> +            if shape.HasTextFrame and shape.TextFrame.HasText:
> +                text += shape.TextFrame.TextRange.Text + '\n'
>      return text
> 
> === modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
> --- openlp/plugins/presentations/lib/pptviewcontroller.py	2014-08-27 23:18:06 +0000
> +++ openlp/plugins/presentations/lib/pptviewcontroller.py	2014-09-21 19:28:31 +0000
> @@ -29,6 +29,11 @@
>  
>  import logging
>  import os
> +import logging
> +import zipfile
> +import re
> +from xml.etree import ElementTree
> +
>  
>  from openlp.core.common import is_win
>  
> @@ -127,14 +132,14 @@
>          temp_folder = self.get_temp_folder()
>          size = ScreenList().current['size']
>          rect = RECT(size.x(), size.y(), size.right(), size.bottom())
> -        file_path = os.path.normpath(self.file_path)
> +        self.file_path = os.path.normpath(self.file_path)
>          preview_path = os.path.join(temp_folder, 'slide')
>          # Ensure that the paths are null terminated
> -        file_path = file_path.encode('utf-16-le') + b'\0'
> +        self.file_path = self.file_path.encode('utf-16-le') + b'\0'
>          preview_path = preview_path.encode('utf-16-le') + b'\0'
>          if not os.path.isdir(temp_folder):
>              os.makedirs(temp_folder)
> -        self.ppt_id = self.controller.process.OpenPPT(file_path, None, rect, preview_path)
> +        self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path)
>          if self.ppt_id >= 0:
>              self.create_thumbnails()
>              self.stop_presentation()
> @@ -154,6 +159,68 @@
>              path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1))
>              self.convert_thumbnail(path, idx + 1)
>  
> +    def create_titles_and_notes(self):
> +        """
> +        Extracts the titles and notes from the zipped file
> +        and writes the list of titles (one per slide)
> +        to 'titles.txt'
> +        and the notes to 'slideNotes[x].txt'
> +        in the thumbnails directory
> +        """
> +        titles = None
> +        notes = None
> +        filename = os.path.normpath(self.file_path)
> +        # let's make sure we have a valid zipped presentation
> +        if os.path.exists(filename) and zipfile.is_zipfile(filename):
> +            namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main";,
> +                          "a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
> +            # open the file
> +            with zipfile.ZipFile(filename) as zip_file:
> +                # find the presentation.xml to get the slide count
> +                with zip_file.open('ppt/presentation.xml') as pres:
> +                    tree = ElementTree.parse(pres)
> +                nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces)
> +                # initialize the lists
> +                titles = ['' for i in range(len(nodes))]
> +                notes = ['' for i in range(len(nodes))]
> +                # loop thru the file list to find slides and notes
> +                for zip_info in zip_file.infolist():
> +                    node_type = ''
> +                    index = -1
> +                    list_to_add = None
> +                    # check if it is a slide
> +                    match = re.search("slides/slide(.+)\.xml", zip_info.filename)
> +                    if match:
> +                        index = int(match.group(1))-1
> +                        node_type = 'ctrTitle'
> +                        list_to_add = titles
> +                    # or a note
> +                    match = re.search("notesSlides/notesSlide(.+)\.xml", zip_info.filename)
> +                    if match:
> +                        index = int(match.group(1))-1
> +                        node_type = 'body'
> +                        list_to_add = notes
> +                    # if it is one of our files, index shouldn't be -1
> +                    if index >= 0:
> +                        with zip_file.open(zip_info) as zipped_file:
> +                            tree = ElementTree.parse(zipped_file)
> +                        text = ''
> +                        nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t",
> +                                                       namespaces=namespaces)
> +                        # if we found any content
> +                        if nodes and len(nodes) > 0:
> +                            for node in nodes:
> +                                if len(text) > 0:
> +                                    text += '\n'
> +                                text += node.text
> +                        # Let's remove the \n from the titles and
> +                        # just add one at the end
> +                        if node_type == 'ctrTitle':
> +                            text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n'
> +                        list_to_add[index] = text
> +        # now let's write the files
> +        self.save_titles_and_notes(titles, notes)
> +
>      def close_presentation(self):
>          """
>          Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
> 
> === added file 'openlp/plugins/presentations/lib/pptviewlib/test.pptx'
> Binary files openlp/plugins/presentations/lib/pptviewlib/test.pptx	1970-01-01 00:00:00 +0000 and openlp/plugins/presentations/lib/pptviewlib/test.pptx	2014-09-21 19:28:31 +0000 differ
> === modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
> --- openlp/plugins/presentations/lib/presentationcontroller.py	2014-05-02 06:42:17 +0000
> +++ openlp/plugins/presentations/lib/presentationcontroller.py	2014-09-21 19:28:31 +0000
> @@ -293,6 +293,49 @@
>          """
>          return ''
>  
> +    def get_titles_and_notes(self):
> +        """
> +        Reads the titles from the titles file and
> +        the notes files and returns the content in two lists
> +        """
> +        titles = []
> +        notes = []
> +        titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
> +        if os.path.exists(titles_file):
> +            try:
> +                with open(titles_file) as fi:
> +                    titles = fi.read().splitlines()
> +            except:
> +                log.exception('Failed to open/read existing titles file')
> +                titles = []
> +        for slide_no, title in enumerate(titles, 1):
> +            notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
> +            note = ''
> +            if os.path.exists(notes_file):
> +                try:
> +                    with open(notes_file) as fn:
> +                        note = fn.read()
> +                except:
> +                    log.exception('Failed to open/read notes file')
> +                    note = ''
> +            notes.append(note)
> +        return titles, notes
> +
> +    def save_titles_and_notes(self, titles, notes):
> +        """
> +        Performs the actual persisting of titles to the titles.txt
> +        and notes to the slideNote%.txt
> +        """
> +        if titles:
> +            titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
> +            with open(titles_file, mode='w') as fo:
> +                fo.writelines(titles)
> +        if notes:
> +            for slide_no, note in enumerate(notes, 1):
> +                notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
> +                with open(notes_file, mode='w') as fn:
> +                    fn.write(note)
> +
>  
>  class PresentationController(object):
>      """
> @@ -427,3 +470,12 @@
>  
>      def close_presentation(self):
>          pass
> +
> +
> +class TextType(object):
> +    """
> +    Type Enumeration for Types of Text to request
> +    """
> +    Title = 0
> +    SlideText = 1
> +    Notes = 2
> 
> === modified file 'openlp/plugins/remotes/html/openlp.js'
> --- openlp/plugins/remotes/html/openlp.js	2013-12-24 08:56:50 +0000
> +++ openlp/plugins/remotes/html/openlp.js	2014-09-21 19:28:31 +0000
> @@ -87,16 +87,30 @@
>          var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
>          ul.html("");
>          for (idx in data.results.slides) {
> -          var text = data.results.slides[idx]["tag"];
> -          if (text != "") text = text + ": ";
> -          text = text + data.results.slides[idx]["text"];
> +          var indexInt = parseInt(idx,10);
> +          var slide = data.results.slides[idx];
> +          var text = slide["tag"];
> +          if (text != "") {
> +            text = text + ": ";
> +          }
> +          if (slide["title"]) {
> +            text += slide["title"]
> +          } else {
> +            text += slide["text"];
> +          }
> +          if (slide["notes"]) {
> +            text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["notes"] + "</div>");
> +          }
>            text = text.replace(/\n/g, '<br />');
> -          var li = $("<li data-icon=\"false\">").append(
> -            $("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text));
> -          if (data.results.slides[idx]["selected"]) {
> +          if (slide["img"]) {
> +            text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails88x88/") + "'>";
> +          }
> +          var li = $("<li data-icon=\"false\">").append($("<a href=\"#\">").html(text));
> +          if (slide["selected"]) {
>              li.attr("data-theme", "e");
>            }
>            li.children("a").click(OpenLP.setSlide);
> +          li.find("*").attr("value", indexInt );
>            ul.append(li);
>          }
>          OpenLP.currentItem = data.results.item;
> 
> === modified file 'openlp/plugins/remotes/html/stage.js'
> --- openlp/plugins/remotes/html/stage.js	2013-12-24 08:56:50 +0000
> +++ openlp/plugins/remotes/html/stage.js	2014-09-21 19:28:31 +0000
> @@ -102,7 +102,21 @@
>      $("#verseorder span").removeClass("currenttag");
>      $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
>      var slide = OpenLP.currentSlides[OpenLP.currentSlide];
> -    var text = slide["text"];
> +    var text = "";
> +    // use title if available
> +    if (slide["title"]) {
> +        text = slide["title"];
> +    } else {
> +        text = slide["text"];
> +    }
> +    // use thumbnail if available
> +    if (slide["img"]) {
> +        text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
> +    }
> +    // use notes if available
> +    if (slide["notes"]) {
> +        text += '<br />' + slide["notes"];
> +    }
>      text = text.replace(/\n/g, "<br />");
>      $("#currentslide").html(text);
>      text = "";
> @@ -110,7 +124,11 @@
>        for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
>          if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
>              text = text + "<p class=\"nextslide\">";
> -        text = text + OpenLP.currentSlides[idx]["text"];
> +        if (OpenLP.currentSlides[idx]["title"]) {
> +            text = text + OpenLP.currentSlides[idx]["title"];
> +        } else {
> +            text = text + OpenLP.currentSlides[idx]["text"];
> +        }
>          if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
>              text = text + "</p>";
>          else
> 
> === modified file 'openlp/plugins/remotes/lib/httprouter.py'
> --- openlp/plugins/remotes/lib/httprouter.py	2014-04-19 05:09:54 +0000
> +++ openlp/plugins/remotes/lib/httprouter.py	2014-09-21 19:28:31 +0000
> @@ -125,7 +125,7 @@
>  from PyQt4 import QtCore
>  
>  from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate
> -from openlp.core.lib import PluginStatus, StringContent, image_to_byte
> +from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities
>  
>  log = logging.getLogger(__name__)
>  FILE_TYPES = {
> @@ -159,6 +159,7 @@
>              ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
>              ('^/(main)$', {'function': self.serve_file, 'secure': False}),
>              (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
> +            (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
>              (r'^/api/poll$', {'function': self.poll, 'secure': False}),
>              (r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
>              (r'^/main/image$', {'function': self.main_image, 'secure': False}),
> @@ -328,7 +329,8 @@
>              'no_results': translate('RemotePlugin.Mobile', 'No Results'),
>              'options': translate('RemotePlugin.Mobile', 'Options'),
>              'service': translate('RemotePlugin.Mobile', 'Service'),
> -            'slides': translate('RemotePlugin.Mobile', 'Slides')
> +            'slides': translate('RemotePlugin.Mobile', 'Slides'),
> +            'settings': translate('RemotePlugin.Mobile', 'Settings'),
>          }
>  
>      def serve_file(self, file_name=None):
> @@ -380,6 +382,42 @@
>          content_type = FILE_TYPES.get(ext, 'text/plain')
>          return ext, content_type
>  
> +    def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
> +        """
> +        Serve an image file. If not found return 404.
> +        """
> +        log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
> +        supported_controllers = ['presentations', 'images']
> +        # -1 means use the default dimension in ImageManager
> +        width = -1
> +        height = -1
> +        if dimensions:
> +            match = re.search('(\d+)x(\d+)', dimensions)
> +            if match:
> +                # let's make sure that the dimensions are within reason
> +                width = sorted([10, int(match.group(1)), 1000])[1]
> +                height = sorted([10, int(match.group(2)), 1000])[1]
> +        content = ''
> +        content_type = None
> +        if controller_name and file_name:
> +            if controller_name in supported_controllers:
> +                full_path = urllib.parse.unquote(file_name)
> +                if '..' not in full_path:  # no hacking please
> +                    full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
> +                                                              'thumbnails/' + full_path))
> +                    if os.path.exists(full_path):
> +                        path, just_file_name = os.path.split(full_path)
> +                        self.image_manager.add_image(full_path, just_file_name, None, width, height)
> +                        ext, content_type = self.get_content_type(full_path)
> +                        image = self.image_manager.get_image(full_path, just_file_name, width, height)
> +                        content = image_to_byte(image, False)
> +        if len(content) == 0:
> +            return self.do_not_found()
> +        self.send_response(200)
> +        self.send_header('Content-type', content_type)
> +        self.end_headers()
> +        return content
> +
>      def poll(self):
>          """
>          Poll OpenLP to determine the current slide number and item name.
> @@ -458,6 +496,7 @@
>          if current_item:
>              for index, frame in enumerate(current_item.get_frames()):
>                  item = {}
> +                # Handle text (songs, custom)

and Bibles!

>                  if current_item.is_text():
>                      if frame['verseTag']:
>                          item['tag'] = str(frame['verseTag'])
> @@ -465,11 +504,33 @@
>                          item['tag'] = str(index + 1)
>                      item['text'] = str(frame['text'])
>                      item['html'] = str(frame['html'])
> +                # Handle images, unless a custom thumbnail is given or if thumbnails is disabled
> +                elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'):
> +                    item['tag'] = str(index + 1)
> +                    thumbnail_path = os.path.sep + os.path.join('images', 'thumbnails', frame['title'])
> +                    item['img'] = urllib.request.pathname2url(thumbnail_path)
> +                    item['text'] = str(frame['title'])
> +                    item['html'] = str(frame['title'])
>                  else:
> +                    # Handle presentation etc.
>                      item['tag'] = str(index + 1)
> +                    if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
> +                        item['title'] = str(frame['display_title'])
> +                    if current_item.is_capable(ItemCapabilities.HasNotes):
> +                        item['notes'] = str(frame['notes'])
> +                    if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
> +                            Settings().value('remotes/thumbnails'):
> +                        # If the file is under our app directory tree send the portion after the match
> +                        data_path = AppLocation.get_data_path()
> +                        print(frame)
> +                        if frame['image'][0:len(data_path)] == data_path:
> +                            item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
>                      item['text'] = str(frame['title'])
>                      item['html'] = str(frame['title'])
>                  item['selected'] = (self.live_controller.selected_row == index)
> +                if current_item.notes:
> +                    item['notes'] = item.get('notes', '') + '\n' + current_item.notes
> +                print(item)
>                  data.append(item)
>          json_data = {'results': {'slides': data}}
>          if current_item:
> 
> === modified file 'openlp/plugins/remotes/lib/httpserver.py'
> --- openlp/plugins/remotes/lib/httpserver.py	2014-04-19 05:26:49 +0000
> +++ openlp/plugins/remotes/lib/httpserver.py	2014-09-21 19:28:31 +0000
> @@ -144,6 +144,7 @@
>              try:
>                  self.httpd = server_class((address, port), CustomHandler)
>                  log.debug("Server started for class %s %s %d" % (server_class, address, port))
> +                break
>              except OSError:
>                  log.debug("failed to start http server thread state %d %s" %
>                            (loop, self.http_thread.isRunning()))
> @@ -151,6 +152,8 @@
>                  time.sleep(0.1)
>              except:
>                  log.error('Failed to start server ')
> +                loop += 1
> +                time.sleep(0.1)
>  
>      def stop_server(self):
>          """
> 
> === modified file 'openlp/plugins/remotes/lib/remotetab.py'
> --- openlp/plugins/remotes/lib/remotetab.py	2014-04-20 20:16:08 +0000
> +++ openlp/plugins/remotes/lib/remotetab.py	2014-09-21 19:28:31 +0000
> @@ -62,6 +62,9 @@
>          self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
>          self.twelve_hour_check_box.setObjectName('twelve_hour_check_box')
>          self.server_settings_layout.addRow(self.twelve_hour_check_box)
> +        self.thumbnails_check_box = QtGui.QCheckBox(self.server_settings_group_box)
> +        self.thumbnails_check_box.setObjectName('thumbnails_check_box')
> +        self.server_settings_layout.addRow(self.thumbnails_check_box)
>          self.left_layout.addWidget(self.server_settings_group_box)
>          self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
>          self.http_settings_group_box.setObjectName('http_settings_group_box')
> @@ -163,6 +166,7 @@
>          self.left_layout.addStretch()
>          self.right_layout.addStretch()
>          self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
> +        self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
>          self.address_edit.textChanged.connect(self.set_urls)
>          self.port_spin_box.valueChanged.connect(self.set_urls)
>          self.https_port_spin_box.valueChanged.connect(self.set_urls)
> @@ -176,6 +180,8 @@
>          self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
>          self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
>          self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
> +        self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
> +                                                    'Show thumbnails of non-text slides in remote and stage view.'))
>          self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
>          self.qr_description_label.setText(
>              translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/'
> @@ -240,6 +246,8 @@
>          self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
>          self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
>          self.twelve_hour_check_box.setChecked(self.twelve_hour)
> +        self.thumbnails = Settings().value(self.settings_section + '/thumbnails')
> +        self.thumbnails_check_box.setChecked(self.thumbnails)
>          local_data = AppLocation.get_directory(AppLocation.DataDir)
>          if not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.crt')) or \
>                  not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.key')):
> @@ -271,6 +279,7 @@
>          Settings().setValue(self.settings_section + '/https enabled', self.https_settings_group_box.isChecked())
>          Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
>          Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
> +        Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
>          Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
>          Settings().setValue(self.settings_section + '/user id', self.user_id.text())
>          Settings().setValue(self.settings_section + '/password', self.password.text())
> @@ -285,6 +294,15 @@
>          if check_state == QtCore.Qt.Checked:
>              self.twelve_hour = True
>  
> +    def on_thumbnails_check_box_changed(self, check_state):
> +        """
> +        Toggle the thumbnail check box.
> +        """
> +        self.thumbnails = False
> +        # we have a set value convert to True/False
> +        if check_state == QtCore.Qt.Checked:
> +            self.thumbnails = True
> +
>      def https_changed(self):
>          """
>          Invert the HTTP group box based on Https group settings
> 
> === modified file 'openlp/plugins/remotes/remoteplugin.py'
> --- openlp/plugins/remotes/remoteplugin.py	2014-04-20 20:16:08 +0000
> +++ openlp/plugins/remotes/remoteplugin.py	2014-09-21 19:28:31 +0000
> @@ -44,7 +44,8 @@
>      'remotes/user id': 'openlp',
>      'remotes/password': 'password',
>      'remotes/authentication enabled': False,
> -    'remotes/ip address': '0.0.0.0'
> +    'remotes/ip address': '0.0.0.0',
> +    'remotes/thumbnails': True
>  }
>  
>  
> 
> === modified file 'tests/functional/openlp_core_common/test_applocation.py'
> --- tests/functional/openlp_core_common/test_applocation.py	2014-04-02 18:51:21 +0000
> +++ tests/functional/openlp_core_common/test_applocation.py	2014-09-21 19:28:31 +0000
> @@ -162,9 +162,9 @@
>                  patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \
>                  patch('openlp.core.common.applocation.os.path.split') as mocked_split, \
>                  patch('openlp.core.common.applocation.sys') as mocked_sys:
> -            mocked_abspath.return_value = 'plugins/dir'
> +            mocked_abspath.return_value = os.path.join('plugins', 'dir')
>              mocked_split.return_value = ['openlp']
> -            mocked_get_frozen_path.return_value = 'plugins/dir'
> +            mocked_get_frozen_path.return_value = os.path.join('plugins', 'dir')
>              mocked_sys.frozen = 1
>              mocked_sys.argv = ['openlp']
>  
> @@ -172,7 +172,7 @@
>              directory = AppLocation.get_directory(AppLocation.PluginsDir)
>  
>              # THEN: The correct directory should be returned
> -            self.assertEqual('plugins/dir', directory, 'Directory should be "plugins/dir"')
> +            self.assertEqual(os.path.join('plugins', 'dir'), directory, 'Directory should be "plugins/dir"')
>  
>      def get_frozen_path_in_unfrozen_app_test(self):
>          """
> 
> === modified file 'tests/functional/openlp_core_lib/test_image_manager.py'
> --- tests/functional/openlp_core_lib/test_image_manager.py	2014-06-04 04:54:44 +0000
> +++ tests/functional/openlp_core_lib/test_image_manager.py	2014-09-21 19:28:31 +0000
> @@ -69,16 +69,17 @@
>          Test the Image Manager setup basic functionality
>          """
>          # GIVEN: the an image add to the image manager
> -        self.image_manager.add_image(TEST_PATH, 'church.jpg', None)
> +        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
> +        self.image_manager.add_image(full_path, 'church.jpg', None)
>  
>          # WHEN the image is retrieved
> -        image = self.image_manager.get_image(TEST_PATH, 'church.jpg')
> +        image = self.image_manager.get_image(full_path, 'church.jpg')
>  
>          # THEN returned record is a type of image
>          self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
>  
>          # WHEN: The image bytes are requested.
> -        byte_array = self.image_manager.get_image_bytes(TEST_PATH, 'church.jpg')
> +        byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg')
>  
>          # THEN: Type should be a str.
>          self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str')
> @@ -89,6 +90,38 @@
>              self.image_manager.get_image(TEST_PATH, 'church1.jpg')
>          self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
>  
> +    def different_dimension_image_test(self):
> +        """
> +        Test the Image Manager with dimensions
> +        """
> +        # GIVEN: add an image with specific dimensions
> +        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
> +        self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
> +
> +        # WHEN: the image is retrieved
> +        image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80)
> +
> +        # THEN: The return should be of type image
> +        self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
> +
> +        # WHEN: adding the same image with different dimensions
> +        self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100)
> +
> +        # THEN: the cache should contain two pictures
> +        self.assertEqual(len(self.image_manager._cache), 2,
> +                         'Image manager should consider two dimensions of the same picture as different')
> +
> +        # WHEN: adding the same image with first dimensions
> +        self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
> +
> +        # THEN: the cache should still contain only two pictures
> +        self.assertEqual(len(self.image_manager._cache), 2, 'Same dimensions should not be added again')
> +
> +        # WHEN: calling with correct image, but wrong dimensions
> +        with self.assertRaises(KeyError) as context:
> +            self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
> +        self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension')
> +
>      def process_cache_test(self):
>          """
>          Test the process_cache method
> @@ -151,7 +184,7 @@
>  
>          :param image: The name of the image. E. g. ``image1``
>          """
> -        return self.image_manager._cache[(TEST_PATH, image)].priority
> +        return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority
>  
>      def mocked_resize_image(self, *args):
>          """
> 
> === modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
> --- tests/functional/openlp_core_lib/test_serviceitem.py	2014-03-17 20:59:58 +0000
> +++ tests/functional/openlp_core_lib/test_serviceitem.py	2014-09-21 19:28:31 +0000
> @@ -32,13 +32,11 @@
>  import os
>  from unittest import TestCase
>  
> -
>  from tests.functional import MagicMock, patch
>  from tests.utils import assert_length, convert_file_service_item
>  
>  from openlp.core.common import Registry
> -from openlp.core.lib import ItemCapabilities, ServiceItem
> -
> +from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
>  
>  VERSE = 'The Lord said to {r}Noah{/r}: \n'\
>          'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
> @@ -113,20 +111,25 @@
>          # GIVEN: A new service item and a mocked add icon function
>          image_name = 'image_1.jpg'
>          test_file = os.path.join(TEST_PATH, image_name)
> -        frame_array = {'path': test_file, 'title': image_name}
> +        thumb_file = os.path.normpath(os.path.join('/path/thumbnails', image_name))
> +        frame_array = {'path': test_file, 'title': image_name, 'image': thumb_file}
>  
>          service_item = ServiceItem(None)
>          service_item.add_icon = MagicMock()
>  
>          # WHEN: adding an image from a saved Service and mocked exists
>          line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj')
> -        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
> +        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\
> +                patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb,\
> +                patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
> +                mocked_get_section_data_path:
>              mocked_exists.return_value = True
> +            mocked_get_section_data_path.return_value = os.path.normpath('/path/')
>              service_item.set_from_service(line, TEST_PATH)
>  
>          # THEN: We should get back a valid service item
>          self.assertTrue(service_item.is_valid, 'The new service item should be valid')
> -        self.assertEqual(test_file, service_item.get_rendered_frame(0),
> +        self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)),
>                           'The first frame should match the path to the image')
>          self.assertEqual(frame_array, service_item.get_frames()[0],
>                           'The return should match frame array1')
> @@ -153,10 +156,12 @@
>          # GIVEN: A new service item and a mocked add icon function
>          image_name1 = 'image_1.jpg'
>          image_name2 = 'image_2.jpg'
> -        test_file1 = os.path.join('/home/openlp', image_name1)
> -        test_file2 = os.path.join('/home/openlp', image_name2)
> -        frame_array1 = {'path': test_file1, 'title': image_name1}
> -        frame_array2 = {'path': test_file2, 'title': image_name2}
> +        test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
> +        test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
> +        thumb_file1 = os.path.normpath(os.path.join('/path/thumbnails', image_name1))
> +        thumb_file2 = os.path.normpath(os.path.join('/path/thumbnails', image_name2))
> +        frame_array1 = {'path': test_file1, 'title': image_name1, 'image': thumb_file1}
> +        frame_array2 = {'path': test_file2, 'title': image_name2, 'image': thumb_file2}
>  
>          service_item = ServiceItem(None)
>          service_item.add_icon = MagicMock()
> @@ -168,8 +173,12 @@
>          line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj')
>          line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1)
>  
> -        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists:
> +        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists, \
> +                patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb, \
> +                patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
> +                mocked_get_section_data_path:
>              mocked_exists.return_value = True
> +            mocked_get_section_data_path.return_value = os.path.normpath('/path/')
>              service_item2.set_from_service(line2)
>              service_item.set_from_service(line)
>  
> @@ -207,6 +216,44 @@
>          self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend),
>                          'This service item should be able to have new items added to it')
>  
> +    def add_from_command_for_a_presentation_test(self):
> +        """
> +        Test the Service Item - adding a presentation
> +        """
> +        # GIVEN: A service item, a mocked icon and presentation data
> +        service_item = ServiceItem(None)
> +        presentation_name = 'test.pptx'
> +        image = MagicMock()
> +        display_title = 'DisplayTitle'
> +        notes = 'Note1\nNote2\n'
> +        frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH,
> +                 'display_title': display_title, 'notes': notes}
> +
> +        # WHEN: adding presentation to service_item
> +        service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes)
> +
> +        # THEN: verify that it is setup as a Command and that the frame data matches
> +        self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
> +        self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
> +
> +    def add_from_comamnd_without_display_title_and_notes_test(self):
> +        """
> +        Test the Service Item - add from command, but not presentation
> +        """
> +        # GIVEN: A new service item, a mocked icon and image data
> +        service_item = ServiceItem(None)
> +        image_name = 'test.img'
> +        image = MagicMock()
> +        frame = {'title': image_name, 'image': image, 'path': TEST_PATH,
> +                 'display_title': None, 'notes': None}
> +
> +        # WHEN: adding image to service_item
> +        service_item.add_from_command(TEST_PATH, image_name, image)
> +
> +        # THEN: verify that it is setup as a Command and that the frame data matches
> +        self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
> +        self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
> +
>      def service_item_load_optical_media_from_service_test(self):
>          """
>          Test the Service Item - load an optical media item
> 
> === modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
> --- tests/functional/openlp_core_ui/test_thememanager.py	2014-08-21 13:07:02 +0000
> +++ tests/functional/openlp_core_ui/test_thememanager.py	2014-09-21 19:28:31 +0000
> @@ -57,17 +57,18 @@
>          # GIVEN: A new ThemeManager instance.
>          theme_manager = ThemeManager()
>          theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes')
> -        zipfile.ZipFile.__init__ = MagicMock()
> -        zipfile.ZipFile.__init__.return_value = None
> -        zipfile.ZipFile.write = MagicMock()
> -
> -        # WHEN: The theme is exported
> -        theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
> -
> -        # THEN: The zipfile should be created at the given path
> -        zipfile.ZipFile.__init__.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
> -        zipfile.ZipFile.write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'),
> -                                                 os.path.join('Default', 'Default.xml'))
> +        with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
> +                patch('zipfile.ZipFile.write') as mocked_zipfile_write:
> +            mocked_zipfile_init.return_value = None
> +
> +            # WHEN: The theme is exported
> +            theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
> +
> +            # THEN: The zipfile should be created at the given path
> +            mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
> +            mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes',
> +                                                                 'Default', 'Default.xml'),
> +                                                    os.path.join('Default', 'Default.xml'))
>  
>      def initial_theme_manager_test(self):
>          """
> 
> === added file 'tests/functional/openlp_plugins/presentations/test_impresscontroller.py'
> --- tests/functional/openlp_plugins/presentations/test_impresscontroller.py	1970-01-01 00:00:00 +0000
> +++ tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2014-09-21 19:28:31 +0000
> @@ -0,0 +1,229 @@
> +# -*- coding: utf-8 -*-
> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
> +
> +###############################################################################
> +# OpenLP - Open Source Lyrics Projection                                      #
> +# --------------------------------------------------------------------------- #
> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
> +# --------------------------------------------------------------------------- #
> +# 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                          #
> +###############################################################################
> +"""
> +Functional tests to test the Impress class and related methods.
> +"""
> +from unittest import TestCase
> +import os
> +import shutil
> +from tempfile import mkdtemp
> +
> +from tests.functional import patch, MagicMock
> +from tests.utils.constants import TEST_RESOURCES_PATH
> +from tests.helpers.testmixin import TestMixin
> +
> +from openlp.plugins.presentations.lib.impresscontroller import \
> +    ImpressController, ImpressDocument, TextType
> +
> +
> +class TestImpressController(TestCase, TestMixin):
> +    """
> +    Test the ImpressController Class
> +    """
> +
> +    def setUp(self):
> +        """
> +        Set up the patches and mocks need for all tests.
> +        """
> +        self.get_application()
> +        self.build_settings()
> +        self.mock_plugin = MagicMock()
> +        self.temp_folder = mkdtemp()
> +        self.mock_plugin.settings_section = self.temp_folder
> +
> +    def tearDown(self):
> +        """
> +        Stop the patches
> +        """
> +        self.destroy_settings()
> +        shutil.rmtree(self.temp_folder)
> +
> +    def constructor_test(self):
> +        """
> +        Test the Constructor from the ImpressController
> +        """
> +        # GIVEN: No presentation controller
> +        controller = None
> +
> +        # WHEN: The presentation controller object is created
> +        controller = ImpressController(plugin=self.mock_plugin)
> +
> +        # THEN: The name of the presentation controller should be correct
> +        self.assertEqual('Impress', controller.name,
> +                         'The name of the presentation controller should be correct')
> +
> +
> +class TestImpressDocumnt(TestCase):

Document

> +    """
> +    Test the ImpressDocument Class
> +    """
> +    def setUp(self):
> +        mocked_plugin = MagicMock()
> +        mocked_plugin.settings_section = 'presentations'
> +        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
> +        self.ppc = ImpressController(mocked_plugin)
> +        self.doc = ImpressDocument(self.ppc, self.file_name)
> +
> +    def create_titles_and_notes_test(self):
> +        """
> +        Test ImpressDocument.create_titles_and_notes
> +        """
> +        # GIVEN: mocked PresentationController.save_titles_and_notes with
> +        # 0 pages and the LibreOffice Document
> +        self.doc.save_titles_and_notes = MagicMock()
> +        self.doc.document = MagicMock()
> +        self.doc.document.getDrawPages.return_value = MagicMock()
> +        self.doc.document.getDrawPages().getCount.return_value = 0
> +
> +        # WHEN reading the titles and notes
> +        self.doc.create_titles_and_notes()
> +
> +        # THEN save_titles_and_notes should have been called with empty arrays
> +        self.doc.save_titles_and_notes.assert_called_once_with([], [])
> +
> +        # GIVEN: reset mock and set it to 2 pages
> +        self.doc.save_titles_and_notes.reset_mock()
> +        self.doc.document.getDrawPages().getCount.return_value = 2
> +
> +        # WHEN: a new call to create_titles_and_notes
> +        self.doc.create_titles_and_notes()
> +
> +        # THEN: save_titles_and_notes should have been called once with
> +        # two arrays of two elements
> +        self.doc.save_titles_and_notes.assert_called_once_with(['\n', '\n'], [' ', ' '])
> +
> +    def get_text_from_page_out_of_bound_test(self):
> +        """
> +        Test ImpressDocument.__get_text_from_page with out-of-bounds index
> +        """
> +        # GIVEN: mocked LibreOffice Document with one slide,
> +        # two notes and three texts
> +        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
> +
> +        # WHEN: __get_text_from_page is called with an index of 0x00
> +        result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Notes)
> +
> +        # THEN: the result should be an empty string
> +        self.assertEqual(result, '', 'Result should be an empty string')
> +
> +        # WHEN: regardless of the type of text, index 0x00 is out of bounds
> +        result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Title)
> +
> +        # THEN: result should be an empty string
> +        self.assertEqual(result, '', 'Result should be an empty string')
> +
> +        # WHEN: when called with 2, it should also be out of bounds
> +        result = self.doc._ImpressDocument__get_text_from_page(2, TextType.SlideText)
> +
> +        # THEN: result should be an empty string ... and, getByIndex should
> +        # have never been called
> +        self.assertEqual(result, '', 'Result should be an empty string')
> +        self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
> +                         'There should be no call to getByIndex')
> +
> +    def get_text_from_page_wrong_type_test(self):
> +        """
> +        Test ImpressDocument.__get_text_from_page with wrong TextType
> +        """
> +        # GIVEN: mocked LibreOffice Document with one slide, two notes and
> +        # three texts
> +        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
> +
> +        # WHEN: called with TextType 3
> +        result = self.doc._ImpressDocument__get_text_from_page(1, 3)
> +
> +        # THEN: result should be an empty string
> +        self.assertEqual(result, '', 'Result should be and empty string')
> +        self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
> +                         'There should be no call to getByIndex')
> +
> +    def get_text_from_page_valid_params_test(self):
> +        """
> +        Test ImpressDocument.__get_text_from_page with valid parameters
> +        """
> +        # GIVEN: mocked LibreOffice Document with one slide,
> +        # two notes and three texts
> +        self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
> +
> +        # WHEN: __get_text_from_page is called to get the Notes
> +        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Notes)
> +
> +        # THEN: result should be 'Note\nNote\n'
> +        self.assertEqual(result, 'Note\nNote\n', 'Result should be \'Note\\n\' times the count of notes in the page')
> +
> +        # WHEN: get the Title
> +        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Title)
> +
> +        # THEN: result should be 'Title\n'
> +        self.assertEqual(result, 'Title\n', 'Result should be exactly \'Title\\n\'')
> +
> +        # WHEN: get all text
> +        result = self.doc._ImpressDocument__get_text_from_page(1, TextType.SlideText)
> +
> +        # THEN: result should be 'Title\nString\nString\n'
> +        self.assertEqual(result, 'Title\nString\nString\n', 'Result should be exactly \'Title\\nString\\nString\\n\'')
> +
> +    def _mock_a_LibreOffice_document(self, page_count, note_count, text_count):
> +        """
> +        Helper function, creates a mock libreoffice document.
> +
> +        :param page_count: Number of pages in the document
> +        :param note_count: Number of note pages in the document
> +        :param text_count: Number of text pages in the document
> +        """
> +        pages = MagicMock()
> +        page = MagicMock()
> +        pages.getByIndex.return_value = page
> +        notes_page = MagicMock()
> +        notes_page.getCount.return_value = note_count
> +        shape = MagicMock()
> +        shape.supportsService.return_value = True
> +        shape.getString.return_value = 'Note'
> +        notes_page.getByIndex.return_value = shape
> +        page.getNotesPage.return_value = notes_page
> +        page.getCount.return_value = text_count
> +        page.getByIndex.side_effect = self._get_page_shape_side_effect
> +        pages.getCount.return_value = page_count
> +        document = MagicMock()
> +        document.getDrawPages.return_value = pages
> +        document.getByIndex.return_value = page
> +        return document
> +
> +    def _get_page_shape_side_effect(*args):
> +        """
> +        Helper function.
> +        """
> +        page_shape = MagicMock()
> +        page_shape.supportsService.return_value = True
> +        if args[1] == 0:
> +            page_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
> +            page_shape.getString.return_value = 'Title'
> +        else:
> +            page_shape.getString.return_value = 'String'
> +        return page_shape
> 
> === modified file 'tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py'
> --- tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	2014-07-03 11:21:12 +0000
> +++ tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py	2014-09-21 19:28:31 +0000
> @@ -30,16 +30,20 @@
>  Functional tests to test the PowerPointController class and related methods.
>  """
>  import os
> -if os.name == 'nt':
> -    import pywintypes
>  import shutil
>  from unittest import TestCase
>  from tempfile import mkdtemp
>  
>  from tests.functional import patch, MagicMock
>  from tests.helpers.testmixin import TestMixin
> -
> -from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument
> +from tests.utils.constants import TEST_RESOURCES_PATH
> +
> +from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument,\
> +    _get_text_from_shapes
> +from openlp.core.common import is_win
> +
> +if is_win():
> +    import pywintypes
>  
>  
>  class TestPowerpointController(TestCase, TestMixin):
> @@ -79,7 +83,7 @@
>                           'The name of the presentation controller should be correct')
>  
>  
> -class TestPowerpointDocument(TestCase):
> +class TestPowerpointDocument(TestCase, TestMixin):
>      """
>      Test the PowerpointDocument Class
>      """
> @@ -88,6 +92,11 @@
>          """
>          Set up the patches and mocks need for all tests.
>          """
> +        self.get_application()
> +        self.build_settings()
> +        self.mock_plugin = MagicMock()
> +        self.temp_folder = mkdtemp()
> +        self.mock_plugin.settings_section = self.temp_folder
>          self.powerpoint_document_stop_presentation_patcher = patch(
>              'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation')
>          self.presentation_document_get_temp_folder_patcher = patch(
> @@ -100,6 +109,8 @@
>          self.mock_controller = MagicMock()
>          self.mock_presentation = MagicMock()
>          self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
> +        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
> +        self.real_controller = PowerpointController(self.mock_plugin)
>  
>      def tearDown(self):
>          """
> @@ -108,12 +119,14 @@
>          self.powerpoint_document_stop_presentation_patcher.stop()
>          self.presentation_document_get_temp_folder_patcher.stop()
>          self.presentation_document_setup_patcher.stop()
> +        self.destroy_settings()
> +        shutil.rmtree(self.temp_folder)
>  
>      def show_error_msg_test(self):
>          """
>          Test the PowerpointDocument.show_error_msg() method gets called on com exception
>          """
> -        if os.name == 'nt':
> +        if is_win():
>              # GIVEN: A PowerpointDocument with mocked controller and presentation
>              with patch('openlp.plugins.presentations.lib.powerpointcontroller.critical_error_message_box') as \
>                      mocked_critical_error_message_box:
> @@ -129,3 +142,95 @@
>                                                                       'integration and the presentation will be stopped.'
>                                                                       ' Restart the presentation if you wish to '
>                                                                       'present it.')
> +
> +    # add _test to the following if necessary
> +    def verify_loading_document(self):
> +        """
> +        Test loading a document in PowerPoint
> +        """
> +        if is_win() and self.real_controller.check_available():
> +            # GIVEN: A PowerpointDocument and a presentation
> +            doc = PowerpointDocument(self.real_controller, self.file_name)
> +
> +            # WHEN: loading the filename
> +            doc.load_presentation()
> +            result = doc.is_loaded()
> +
> +            # THEN: result should be true
> +            self.assertEqual(result, True, 'The result should be True')
> +        else:
> +            self.skipTest('Powerpoint not available, skipping test.')
> +
> +    def create_titles_and_notes_test(self):
> +        """
> +        Test creating the titles from PowerPoint
> +        """
> +        if is_win() and self.real_controller.check_available():
> +            # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
> +            self.doc = PowerpointDocument(self.real_controller, self.file_name)
> +            self.doc.save_titles_and_notes = MagicMock()
> +            self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
> +            slide = MagicMock()
> +            slide.Shapes.Title.TextFrame.TextRange.Text = 'SlideText'
> +            pres = MagicMock()
> +            pres.Slides = [slide, slide]
> +            self.doc.presentation = pres
> +
> +            # WHEN reading the titles and notes
> +            self.doc.create_titles_and_notes()
> +
> +            # THEN the save should have been called exactly once with 2 titles and 2 notes
> +            self.doc.save_titles_and_notes.assert_called_once_with(['SlideText\n', 'SlideText\n'], [' ', ' '])
> +        else:
> +            self.skipTest('Powerpoint not available, skipping test.')
> +
> +    def create_titles_and_notes_with_no_slides_test(self):
> +        """
> +        Test creating the titles from PowerPoint when it returns no slides
> +        """
> +        if is_win() and self.real_controller.check_available():
> +            # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
> +            doc = PowerpointDocument(self.real_controller, self.file_name)
> +            doc.save_titles_and_notes = MagicMock()
> +            doc._PowerpointDocument__get_text_from_shapes = MagicMock()
> +            pres = MagicMock()
> +            pres.Slides = []
> +            doc.presentation = pres
> +
> +            # WHEN reading the titles and notes
> +            doc.create_titles_and_notes()
> +
> +            # THEN the save should have been called exactly once with empty titles and notes
> +            doc.save_titles_and_notes.assert_called_once_with([], [])
> +        else:
> +            self.skipTest('Powerpoint not available, skipping test.')
> +
> +    def get_text_from_shapes_test(self):
> +        """
> +        Test getting text from powerpoint shapes
> +        """
> +        # GIVEN: mocked shapes
> +        shape = MagicMock()
> +        shape.PlaceholderFormat.Type = 2
> +        shape.HasTextFrame = shape.TextFrame.HasText = True
> +        shape.TextFrame.TextRange.Text = 'slideText'
> +        shapes = [shape, shape]
> +
> +        # WHEN: getting the text
> +        result = _get_text_from_shapes(shapes)
> +
> +        # THEN: it should return the text
> +        self.assertEqual(result, 'slideText\nslideText\n', 'result should match \'slideText\nslideText\n\'')
> +
> +    def get_text_from_shapes_with_no_shapes_test(self):
> +        """
> +        Test getting text from powerpoint shapes with no shapes
> +        """
> +        # GIVEN: empty shapes array
> +        shapes = []
> +
> +        # WHEN: getting the text
> +        result = _get_text_from_shapes(shapes)
> +
> +        # THEN: it should not fail but return empty string
> +        self.assertEqual(result, '', 'result should be empty')
> 
> === modified file 'tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py'
> --- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-04-20 20:19:21 +0000
> +++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2014-09-21 19:28:31 +0000
> @@ -4,8 +4,8 @@
>  ###############################################################################
>  # OpenLP - Open Source Lyrics Projection                                      #
>  # --------------------------------------------------------------------------- #
> -# Copyright (c) 2008-2013 Raoul Snyman                                        #
> -# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan      #
> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
>  # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
>  # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
>  # Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
> @@ -31,16 +31,19 @@
>  """
>  import os
>  import shutil
> -if os.name == 'nt':
> -    from ctypes import cdll
>  
>  from tempfile import mkdtemp
>  from unittest import TestCase
>  
>  from tests.functional import MagicMock, patch
>  from tests.helpers.testmixin import TestMixin
> +from tests.utils.constants import TEST_RESOURCES_PATH
>  
>  from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
> +from openlp.core.common import is_win
> +
> +if is_win():
> +    from ctypes import cdll
>  
>  
>  class TestPptviewController(TestCase, TestMixin):
> @@ -98,7 +101,7 @@
>              available = controller.check_available()
>  
>              # THEN: On windows it should return True, on other platforms False
> -            if os.name == 'nt':
> +            if is_win():
>                  self.assertTrue(available, 'check_available should return True on windows.')
>              else:
>                  self.assertFalse(available, 'check_available should return False when not on windows.')
> @@ -130,7 +133,7 @@
>          """
>          Set up the patches and mocks need for all tests.
>          """
> -        self.os_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os')
> +        self.os_isdir_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.isdir')
>          self.pptview_document_create_thumbnails_patcher = patch(
>              'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
>          self.pptview_document_stop_presentation_patcher = patch(
> @@ -141,46 +144,45 @@
>              'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
>          self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
>          self.rect_patcher = MagicMock()
> -
> -        self.mock_os = self.os_patcher.start()
> +        self.mock_os_isdir = self.os_isdir_patcher.start()
>          self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start()
>          self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start()
>          self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
>          self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
>          self.mock_rect = self.rect_patcher.start()
>          self.mock_screen_list = self.screen_list_patcher.start()
> -
>          self.mock_controller = MagicMock()
>          self.mock_presentation = MagicMock()
> -
> -        self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
> +        self.temp_folder = mkdtemp()
> +        self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
>  
>      def tearDown(self):
>          """
>          Stop the patches
>          """
> -        self.os_patcher.stop()
> +        self.os_isdir_patcher.stop()
>          self.pptview_document_create_thumbnails_patcher.stop()
>          self.pptview_document_stop_presentation_patcher.stop()
>          self.presentation_document_get_temp_folder_patcher.stop()
>          self.presentation_document_setup_patcher.stop()
>          self.rect_patcher.stop()
>          self.screen_list_patcher.stop()
> +        shutil.rmtree(self.temp_folder)
>  
>      def load_presentation_succesfull_test(self):
>          """
>          Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
>          """
>          # GIVEN: A reset mocked_os
> -        self.mock_os.reset()
> +        self.mock_os_isdir.reset()
>  
>          # WHEN: The temporary directory exists and OpenPPT returns successfully (not -1)
> -        self.mock_os.path.isdir.return_value = True
> +        self.mock_os_isdir.return_value = True
>          self.mock_controller.process.OpenPPT.return_value = 0
>          instance = PptviewDocument(self.mock_controller, self.mock_presentation)
>          instance.file_path = 'test\path.ppt'
>  
> -        if os.name == 'nt':
> +        if is_win():
>              result = instance.load_presentation()
>  
>              # THEN: PptviewDocument.load_presentation should return True
> @@ -191,17 +193,78 @@
>          Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
>          not successfully opened
>          """
> -        # GIVEN: A reset mocked_os
> -        self.mock_os.reset()
> +        # GIVEN: A reset mock_os_isdir
> +        self.mock_os_isdir.reset()
>  
>          # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
> -        self.mock_os.path.isdir.return_value = False
> -        self.mock_controller.process.OpenPPT.return_value = -1
> -        instance = PptviewDocument(self.mock_controller, self.mock_presentation)
> -        instance.file_path = 'test\path.ppt'
> -        if os.name == 'nt':
> -            result = instance.load_presentation()
> -
> -            # THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False
> -            self.mock_os.makedirs.assert_called_once_with('temp folder')
> -            self.assertFalse(result)
> +        with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs:
> +            self.mock_os_isdir.return_value = False
> +            self.mock_controller.process.OpenPPT.return_value = -1
> +            instance = PptviewDocument(self.mock_controller, self.mock_presentation)
> +            instance.file_path = 'test\path.ppt'
> +            if is_win():
> +                result = instance.load_presentation()
> +
> +                # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
> +                mock_makedirs.assert_called_once_with(self.temp_folder)
> +                self.assertFalse(result)
> +
> +    def create_titles_and_notes_test(self):
> +        """
> +        Test PowerpointController.create_titles_and_notes
> +        """
> +        # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
> +        doc = PptviewDocument(self.mock_controller, self.mock_presentation)
> +        doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
> +        doc.save_titles_and_notes = MagicMock()
> +
> +        # WHEN reading the titles and notes
> +        doc.create_titles_and_notes()
> +
> +        # THEN save_titles_and_notes should have been called once with empty arrays
> +        doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'],
> +                                                          ['Notes for slide 1', 'Inserted', 'Notes for slide 2',
> +                                                           'Notes \nfor slide 4', 'Notes for slide 3'])
> +
> +    def create_titles_and_notes_nonexistent_file_test(self):
> +        """
> +        Test PowerpointController.create_titles_and_notes with nonexistent file
> +        """
> +        # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
> +        with patch('builtins.open') as mocked_open, \
> +                patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
> +                patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
> +                mocked_dir_exists:
> +            mocked_exists.return_value = False
> +            mocked_dir_exists.return_value = False
> +            doc = PptviewDocument(self.mock_controller, self.mock_presentation)
> +            doc.file_path = 'Idontexist.pptx'
> +            doc.save_titles_and_notes = MagicMock()
> +
> +            # WHEN: Reading the titles and notes
> +            doc.create_titles_and_notes()
> +
> +            # THEN: File existens should have been checked, and not have been opened.
> +            doc.save_titles_and_notes.assert_called_once_with(None, None)
> +            mocked_exists.assert_any_call('Idontexist.pptx')
> +            self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.')
> +
> +    def create_titles_and_notes_invalid_file_test(self):
> +        """
> +        Test PowerpointController.create_titles_and_notes with invalid file
> +        """
> +        # GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file
> +        with patch('builtins.open') as mocked_open, \
> +                patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
> +            mocked_is_zf.return_value = False
> +            mocked_open.filesize = 10
> +            doc = PptviewDocument(self.mock_controller, self.mock_presentation)
> +            doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
> +            doc.save_titles_and_notes = MagicMock()
> +
> +            # WHEN: reading the titles and notes
> +            doc.create_titles_and_notes()
> +
> +            # THEN:
> +            doc.save_titles_and_notes.assert_called_once_with(None, None)
> +            self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once')
> 
> === modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
> --- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2014-03-13 20:59:10 +0000
> +++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2014-09-21 19:28:31 +0000
> @@ -1,158 +1,166 @@
> -# -*- coding: utf-8 -*-
> -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
> -
> -###############################################################################
> -# OpenLP - Open Source Lyrics Projection                                      #
> -# --------------------------------------------------------------------------- #
> -# Copyright (c) 2008-2014 Raoul Snyman                                        #
> -# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
> -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
> -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
> -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
> -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
> -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
> -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
> -# --------------------------------------------------------------------------- #
> -# 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                          #
> -###############################################################################
> -"""
> -This module contains tests for the Presentation Controller.
> -"""
> -from unittest import TestCase
> -
> -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
> -from tests.functional import MagicMock, patch
> -
> -
> -class TestPresentationController(TestCase):
> -    """
> -    Test the PresentationController.
> -    """
> -    # TODO: Items left to test
> -    #   PresentationController
> -    #       __init__
> -    #       enabled
> -    #       is_available
> -    #       check_available
> -    #       start_process
> -    #       kill
> -    #       add_document
> -    #       remove_doc
> -    #       close_presentation
> -    #       _get_plugin_manager
> -
> -    def constructor_test(self):
> -        """
> -        Test the Constructor
> -        """
> -        # GIVEN: No presentation controller
> -        controller = None
> -
> -        # WHEN: The presentation controller object is created
> -        mock_plugin = MagicMock()
> -        mock_plugin.settings_section = ''
> -        controller = PresentationController(plugin=mock_plugin)
> -
> -        # THEN: The name of the presentation controller should be correct
> -        self.assertEqual('PresentationController', controller.name,
> -                         'The name of the presentation controller should be correct')
> -
> -
> -class TestPresentationDocument(TestCase):
> -    """
> -    Test the PresentationDocument Class
> -    """
> -    # TODO: Items left to test
> -    #   PresentationDocument
> -    #       __init__
> -    #       load_presentation
> -    #       presentation_deleted
> -    #       get_file_name
> -    #       get_thumbnail_folder
> -    #       get_temp_folder
> -    #       check_thumbnails
> -    #       close_presentation
> -    #       is_active
> -    #       is_loaded
> -    #       blank_screen
> -    #       unblank_screen
> -    #       is_blank
> -    #       stop_presentation
> -    #       start_presentation
> -    #       get_slide_number
> -    #       get_slide_count
> -    #       goto_slide
> -    #       next_step
> -    #       previous_step
> -    #       convert_thumbnail
> -    #       get_thumbnail_path
> -    #       poll_slidenumber
> -    #       get_slide_text
> -    #       get_slide_notes
> -
> -    def setUp(self):
> -        """
> -        Set up the patches and mocks need for all tests.
> -        """
> -        self.check_directory_exists_patcher = \
> -            patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists')
> -        self.get_thumbnail_folder_patcher = \
> -            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
> -        self._setup_patcher = \
> -            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
> -
> -        self.mock_check_directory_exists = self.check_directory_exists_patcher.start()
> -        self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
> -        self.mock_setup = self._setup_patcher.start()
> -
> -        self.mock_controller = MagicMock()
> -
> -        self.mock_get_thumbnail_folder.return_value = 'returned/path/'
> -
> -    def tearDown(self):
> -        """
> -        Stop the patches
> -        """
> -        self.check_directory_exists_patcher.stop()
> -        self.get_thumbnail_folder_patcher.stop()
> -        self._setup_patcher.stop()
> -
> -    def initialise_presentation_document_test(self):
> -        """
> -        Test the PresentationDocument __init__ method when initialising the PresentationDocument Class
> -        """
> -        # GIVEN: A reset mock_setup and mocked controller
> -        self.mock_setup.reset()
> -
> -        # WHEN: Creating an instance of PresentationDocument
> -        PresentationDocument(self.mock_controller, 'Name')
> -
> -        # THEN: PresentationDocument.__init__ should have been called with the correct arguments
> -        self.mock_setup.assert_called_once_with('Name')
> -
> -    def presentation_document_setup_test(self):
> -        """
> -        Test the PresentationDocument _setup method when initialising the PresentationDocument Class
> -        """
> -        self._setup_patcher.stop()
> -
> -        # GIVEN: A  mocked controller, patched check_directory_exists_patcher and patched get_thumbnail_folder method
> -
> -        # WHEN: Creating an instance of PresentationDocument
> -        PresentationDocument(self.mock_controller, 'Name')
> -
> -        # THEN: check_directory_exists should have been called with the correct arguments
> -        self.mock_check_directory_exists.assert_called_once_with('returned/path/')
> -
> -        self._setup_patcher.start()
> +# -*- coding: utf-8 -*-
> +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
> +
> +###############################################################################
> +# OpenLP - Open Source Lyrics Projection                                      #
> +# --------------------------------------------------------------------------- #
> +# Copyright (c) 2008-2014 Raoul Snyman                                        #
> +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan      #
> +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
> +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
> +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
> +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
> +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
> +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
> +# --------------------------------------------------------------------------- #
> +# 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                          #
> +###############################################################################
> +"""
> +Functional tests to test the PresentationController and PresentationDocument
> +classes and related methods.
> +"""
> +from unittest import TestCase
> +import os
> +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
> +from tests.functional import MagicMock, patch, mock_open
> +
> +FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder'
> +
> +
> +class TestPresentationController(TestCase):
> +    """
> +    Test the PresentationController.
> +    """
> +    def setUp(self):
> +        mocked_plugin = MagicMock()
> +        mocked_plugin.settings_section = 'presentations'
> +        self.presentation = PresentationController(mocked_plugin)
> +        self.document = PresentationDocument(self.presentation, '')
> +
> +    def constructor_test(self):
> +        """
> +        Test the Constructor
> +        """
> +        # GIVEN: A mocked plugin
> +
> +        # WHEN: The PresentationController is created
> +
> +        # THEN: The name of the presentation controller should be correct
> +        self.assertEqual('PresentationController', self.presentation.name,
> +                         'The name of the presentation controller should be correct')
> +
> +    def save_titles_and_notes_test(self):
> +        """
> +        Test PresentationDocument.save_titles_and_notes method with two valid lists
> +        """
> +        # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
> +        mocked_open = mock_open()
> +        with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
> +            titles = ['uno', 'dos']
> +            notes = ['one', 'two']
> +
> +            # WHEN: calling save_titles_and_notes
> +            mocked_get_thumbnail_folder.return_value = 'test'
> +            self.document.save_titles_and_notes(titles, notes)
> +
> +            # THEN: the last call to open should have been for slideNotes2.txt
> +            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w')
> +            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w')
> +            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w')
> +            self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
> +            mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
> +            mocked_open().write.assert_called_any('one')
> +            mocked_open().write.assert_called_any('two')
> +
> +    def save_titles_and_notes_with_None_test(self):
> +        """
> +        Test PresentationDocument.save_titles_and_notes method with no data
> +        """
> +        # GIVEN: None and an empty list and a mocked open and get_thumbnail_folder
> +        with patch('builtins.open') as mocked_open, patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
> +            titles = None
> +            notes = None
> +
> +            # WHEN: calling save_titles_and_notes
> +            mocked_get_thumbnail_folder.return_value = 'test'
> +            self.document.save_titles_and_notes(titles, notes)
> +
> +            # THEN: No file should have been created
> +            self.assertEqual(mocked_open.call_count, 0, 'No file should be created')
> +
> +    def get_titles_and_notes_test(self):
> +        """
> +        Test PresentationDocument.get_titles_and_notes method
> +        """
> +        # GIVEN: A mocked open, get_thumbnail_folder and exists
> +
> +        with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \
> +                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
> +                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
> +            mocked_get_thumbnail_folder.return_value = 'test'
> +            mocked_exists.return_value = True
> +
> +            # WHEN: calling get_titles_and_notes
> +            result_titles, result_notes = self.document.get_titles_and_notes()
> +
> +            # THEN: it should return two items for the titles and two empty strings for the notes
> +            self.assertIs(type(result_titles), list, 'result_titles should be of type list')
> +            self.assertEqual(len(result_titles), 2, 'There should be two items in the titles')
> +            self.assertIs(type(result_notes), list, 'result_notes should be of type list')
> +            self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
> +            self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened')
> +            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'))
> +            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'))
> +            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'))
> +            self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
> +
> +    def get_titles_and_notes_with_file_not_found_test(self):
> +        """
> +        Test PresentationDocument.get_titles_and_notes method with file not found
> +        """
> +        # GIVEN: A mocked open, get_thumbnail_folder and exists
> +        with patch('builtins.open') as mocked_open, \
> +                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
> +                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
> +            mocked_get_thumbnail_folder.return_value = 'test'
> +            mocked_exists.return_value = False
> +
> +            # WHEN: calling get_titles_and_notes
> +            result_titles, result_notes = self.document.get_titles_and_notes()
> +
> +            # THEN: it should return two empty lists
> +            self.assertIs(type(result_titles), list, 'result_titles should be of type list')
> +            self.assertEqual(len(result_titles), 0, 'there be no titles')
> +            self.assertIs(type(result_notes), list, 'result_notes should be a list')
> +            self.assertEqual(len(result_notes), 0, 'but the list should be empty')
> +            self.assertEqual(mocked_open.call_count, 0, 'No calls to open files')
> +            self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
> +
> +    def get_titles_and_notes_with_file_error_test(self):
> +        """
> +        Test PresentationDocument.get_titles_and_notes method with file errors
> +        """
> +        # GIVEN: A mocked open, get_thumbnail_folder and exists
> +        with patch('builtins.open') as mocked_open, \
> +                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
> +                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
> +            mocked_get_thumbnail_folder.return_value = 'test'
> +            mocked_exists.return_value = True
> +            mocked_open.side_effect = IOError()
> +
> +            # WHEN: calling get_titles_and_notes
> +            result_titles, result_notes = self.document.get_titles_and_notes()
> +
> +            # THEN: it should return two empty lists
> +            self.assertIs(type(result_titles), list, 'result_titles should be a list')
> 
> === modified file 'tests/functional/openlp_plugins/remotes/test_remotetab.py'
> --- tests/functional/openlp_plugins/remotes/test_remotetab.py	2014-03-14 22:08:44 +0000
> +++ tests/functional/openlp_plugins/remotes/test_remotetab.py	2014-09-21 19:28:31 +0000
> @@ -48,7 +48,8 @@
>      'remotes/user id': 'openlp',
>      'remotes/password': 'password',
>      'remotes/authentication enabled': False,
> -    'remotes/ip address': '0.0.0.0'
> +    'remotes/ip address': '0.0.0.0',
> +    'remotes/thumbnails': True
>  }
>  ZERO_URL = '0.0.0.0'
>  TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
> 
> === modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
> --- tests/functional/openlp_plugins/remotes/test_router.py	2014-05-07 20:38:34 +0000
> +++ tests/functional/openlp_plugins/remotes/test_router.py	2014-09-21 19:28:31 +0000
> @@ -30,10 +30,12 @@
>  This module contains tests for the lib submodule of the Remotes plugin.
>  """
>  import os
> +import urllib.request
>  from unittest import TestCase
>  
>  from openlp.core.common import Settings, Registry
>  from openlp.plugins.remotes.lib.httpserver import HttpRouter
> +from urllib.parse import urlparse
>  from tests.functional import MagicMock, patch, mock_open
>  from tests.helpers.testmixin import TestMixin
>  
> @@ -186,3 +188,86 @@
>              self.router.send_response.assert_called_once_with(200)
>              self.router.send_header.assert_called_once_with('Content-type', 'text/html')
>              self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
> +
> +    def serve_thumbnail_without_params_test(self):
> +        """
> +        Test the serve_thumbnail routine without params
> +        """
> +        self.router.send_response = MagicMock()
> +        self.router.send_header = MagicMock()
> +        self.router.end_headers = MagicMock()
> +        self.router.wfile = MagicMock()
> +        self.router.serve_thumbnail()
> +        self.router.send_response.assert_called_once_with(404)
> +        self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
> +        self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
> +
> +    def serve_thumbnail_with_invalid_params_test(self):
> +        """
> +        Test the serve_thumbnail routine with invalid params
> +        """
> +        # GIVEN: Mocked send_header, send_response, end_headers and wfile
> +        self.router.send_response = MagicMock()
> +        self.router.send_header = MagicMock()
> +        self.router.end_headers = MagicMock()
> +        self.router.wfile = MagicMock()
> +
> +        # WHEN: pass a bad controller
> +        self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png')
> +
> +        # THEN: a 404 should be returned
> +        self.assertEqual(len(self.router.send_header.mock_calls), 1, 'One header')
> +        self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response')
> +        self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket')
> +        self.router.send_response.assert_called_once_with(404)
> +
> +        # WHEN: pass a bad filename
> +        self.router.send_response.reset_mock()
> +        self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png')
> +
> +        # THEN: return a 404
> +        self.router.send_response.assert_called_once_with(404)
> +
> +        # WHEN: a dangerous URL is passed
> +        self.router.send_response.reset_mock()
> +        self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png')
> +
> +        # THEN: return a 404
> +        self.router.send_response.assert_called_once_with(404)
> +
> +    def serve_thumbnail_with_valid_params_test(self):
> +        """
> +        Test the serve_thumbnail routine with valid params
> +        """
> +        # GIVEN: Mocked send_header, send_response, end_headers and wfile
> +        self.router.send_response = MagicMock()
> +        self.router.send_header = MagicMock()
> +        self.router.end_headers = MagicMock()
> +        self.router.wfile = MagicMock()
> +        mocked_image_manager = MagicMock()
> +        Registry.create()
> +        Registry().register('image_manager', mocked_image_manager)
> +        file_name = 'another%20test/slide1.png'
> +        full_path = os.path.normpath(os.path.join('thumbnails', file_name))
> +        width = 120
> +        height = 90
> +        with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
> +                patch('builtins.open', mock_open(read_data='123')), \
> +                patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
> +                patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
> +            mocked_exists.return_value = True
> +            mocked_image_to_byte.return_value = '123'
> +            mocked_location.get_section_data_path.return_value = ''
> +
> +            # WHEN: pass good controller and filename
> +            result = self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name)
> +
> +            # THEN: a file should be returned
> +            self.assertEqual(self.router.send_header.call_count, 1, 'One header')
> +            self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
> +            self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
> +            mocked_exists.assert_called_with(urllib.parse.unquote(full_path))
> +            self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once')
> +            mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'),
> +                                                   'slide1.png', None, '120x90')
> +            mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), 'slide1.png', '120x90')
> 
> === added file 'tests/resources/presentations/test.ppt'
> Binary files tests/resources/presentations/test.ppt	1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.ppt	2014-09-21 19:28:31 +0000 differ
> === added file 'tests/resources/presentations/test.pptx'
> Binary files tests/resources/presentations/test.pptx	1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.pptx	2014-09-21 19:28:31 +0000 differ


-- 
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/235415
Your team OpenLP Core is subscribed to branch lp:openlp.


Follow ups

References