← Back to team overview

openlp-core team mailing list archive

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

 

Review: Needs Fixing

Don't take all the comments personally but I had issues before you picked this!

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-07-15 13:42:09 +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-07-15 13:42:09 +0000
> @@ -36,6 +36,7 @@
>  import os
>  import time
>  import queue
> +import re
>  
>  from PyQt4 import QtCore
>  
> @@ -106,7 +107,7 @@
>      """
>      secondary_priority = 0
>  
> -    def __init__(self, path, source, background):
> +    def __init__(self, path, source, background, dimensions=''):

dimensions Why a single string.  This should be individual fields.
This needs a big fix.

>          """
>          Create an image for the :class:`ImageManager`'s cache.
>  
> @@ -124,6 +125,15 @@
>          self.source = source
>          self.background = background
>          self.timestamp = 0
> +        match = re.search('(\d+)x(\d+)', dimensions)
> +        if match:
> +            # let's make sure that the dimensions are within reason
> +            self.width = sorted([10, int(match.group(1)), 1000])[1]
> +            self.height = sorted([10, int(match.group(2)), 1000])[1]
> +        else:
> +            # -1 means use the default dimension in ImageManager
> +            self.width = -1
> +            self.height = -1
>          # 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 +220,13 @@
>                  image.background = background
>                  self._reset_image(image)
>  
> -    def update_image_border(self, path, source, background):
> +    def update_image_border(self, path, source, background, dimensions=''):
>          """
>          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, dimensions)]
>          if image.source == source:
>              image.background = background
>              self._reset_image(image)
> @@ -237,12 +247,12 @@
>          if not self.image_thread.isRunning():
>              self.image_thread.start()
>  
> -    def get_image(self, path, source):
> +    def get_image(self, path, source, dimensions=''):
>          """
>          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, dimensions)]
>          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 +267,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, dimensions=''):
>          """
>          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, dimensions)]
>          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 +282,14 @@
>                  time.sleep(0.1)
>          return image.image_bytes
>  
> -    def add_image(self, path, source, background):
> +    def add_image(self, path, source, background, dimensions=''):
>          """
>          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, dimensions) in self._cache:
> +            image = Image(path, source, background, dimensions)
> +            self._cache[(path, source, dimensions)] = 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 +318,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-04-14 18:28:04 +0000
> +++ openlp/core/lib/serviceitem.py	2014-07-15 13:42:09 +0000
> @@ -111,6 +111,16 @@
>      ``CanEditTitle``
>              The capability to edit the title of the item
>  
> +    ``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
>      CanEdit = 2
> @@ -129,6 +139,9 @@
>      HasBackgroundAudio = 15
>      CanAutoStartForLive = 16
>      CanEditTitle = 17
> +    HasDisplayTitle = 18
> +    HasNotes = 19
> +    HasThumbnails = 20
>  
>  
>  class ServiceItem(RegistryProperties):
> @@ -297,16 +310,19 @@
>          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})
> +        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):
> @@ -350,7 +366,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']})

What happens to old services which pre-date this change.

>          return {'header': service_header, 'data': service_data}
>  
>      def set_from_service(self, service_item, path=None):
> @@ -418,7 +435,8 @@
>                      self.title = text_image['title']
>                  if 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-07-15 13:42:09 +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-07-07 11:17:24 +0000
> +++ openlp/core/ui/servicemanager.py	2014-07-15 13:42:09 +0000
> @@ -1281,7 +1281,14 @@
>              # 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', ' ')
> +                    # oops, it is missing, let's make one up

This should be hidden in the service item not in core code.

> +                    if len(text.strip()) == 0:
> +                        text = '[slide ' + str(count+1) + ']'
> +                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-07-15 13:42:09 +0000
> @@ -867,12 +867,17 @@
>  
>          :param message: remote message to be processed.
>          """
> -        index = int(message[0])
> +        index = 0
> +        if len(message) == 0 or message[0] == 'undefined':

How do we get undefined?

> +            return
> +        else:
> +            index = int(message[0])
>          if not self.service_item:
>              return
>          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 +1047,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 +1060,7 @@
>          """
>          self.preview_widget.change_slide(row)
>          self.update_preview()
> +        self.selected_row = row
>  
>      def update_preview(self):
>          """
> 
> === 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-07-15 13:42:09 +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 = []
> 
> === modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
> --- openlp/plugins/presentations/lib/impresscontroller.py	2014-04-12 20:19:22 +0000
> +++ openlp/plugins/presentations/lib/impresscontroller.py	2014-07-15 13:42:09 +0000
> @@ -63,7 +63,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__)
> @@ -255,6 +255,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):
> @@ -448,22 +449,47 @@
>  
>          :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)
> +        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)
> +        return
> 
> === 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-07-15 13:42:09 +0000
> @@ -295,6 +295,7 @@
>                          i += 1
>                          image_file = 'mainslide%03d.png' % i
>                          image = os.path.join(doc.get_temp_folder(), image_file)
> +                    service_item.add_capability(ItemCapabilities.HasThumbnails)
>                      doc.close_presentation()
>                      return True
>                  else:
> @@ -323,11 +324,25 @@
>                      i = 1
>                      img = doc.get_thumbnail_path(i, True)
>                      if img:
> +                        # Get titles and notes
> +                        titles, notes = doc.get_titles_and_notes()
>                          while img:
> -                            service_item.add_from_command(path, name, img)
> +                            # Use title and note if available
> +                            title = None

Should this be None or ''

> +                            if titles and len(titles) >= i:
> +                                title = titles[i - 1]
> +                            note = None
> +                            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()
> +                        if titles.count('') != len(titles):
> +                            service_item.add_capability(ItemCapabilities.HasDisplayTitle)
> +                        if notes.count('') != len(notes):
> +                            service_item.add_capability(ItemCapabilities.HasNotes)
> +                        service_item.add_capability(ItemCapabilities.HasThumbnails)
>                          return True
>                      else:
>                          # File is no longer present
> 
> === modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
> --- openlp/plugins/presentations/lib/powerpointcontroller.py	2014-07-03 11:21:12 +0000
> +++ openlp/plugins/presentations/lib/powerpointcontroller.py	2014-07-15 13:42:09 +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
> @@ -35,16 +35,17 @@
>  
>  if os.name == 'nt':
>      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__)
>  
>  
> @@ -134,6 +135,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:
> @@ -390,6 +392,29 @@
>          """
>          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)
> +        return

return?

> +
>      def show_error_msg(self):
>          """
>          Stop presentation and display an error message.
> @@ -408,8 +433,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-03-29 19:56:20 +0000
> +++ openlp/plugins/presentations/lib/pptviewcontroller.py	2014-07-15 13:42:09 +0000
> @@ -29,6 +29,11 @@
>  
>  import logging
>  import os
> +import logging
> +import zipfile
> +import re
> +from xml.etree import ElementTree
> +
>  
>  if os.name == 'nt':
>      from ctypes import cdll
> @@ -125,14 +130,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()
> @@ -152,6 +157,69 @@
>              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)
> +        return
> +
>      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-07-15 13:42:09 +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-07-15 13:42:09 +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/index.html'
> --- openlp/plugins/remotes/html/index.html	2013-12-24 08:56:50 +0000
> +++ openlp/plugins/remotes/html/index.html	2014-07-15 13:42:09 +0000
> @@ -120,6 +120,21 @@
>        <a href="#" id="controller-previous" data-role="button" data-icon="arrow-l">${prev}</a>
>        <a href="#" id="controller-next" data-role="button" data-icon="arrow-r" data-iconpos="right">${next}</a>
>      </div>
> +    <div data-role="controlgroup" data-type="horizontal" style="float:left">
> +        <a href="#settings" id="controller-settings" data-role="button" data-icon="gear" data-rel="dialog">${settings}</a>
> +    </div>
> +  </div>
> +</div>
> +<div data-role="page" id="settings">
> +  <div data-role="header" data-position="inline" data-theme="b">
> +    <h1>${settings}</h1>
> +  </div>
> +  <div data-role="content">
> +    Display thumbnails:
> +    <div data-role="controlgroup" data-type="horizontal">
> +        <a href="#" id="display-thumbnails" data-role="button">Yes</a>
> +        <a href="#" id="dont-display-thumbnails" data-role="button">No</a>
> +    </div>
>    </div>
>  </div>
>  <div data-role="page" id="alerts">
> 
> === 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-07-15 13:42:09 +0000
> @@ -87,16 +87,26 @@
>          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"];
> +          var indexInt = parseInt(idx,10);
> +          var slide = data.results.slides[idx];
> +          var text = slide["tag"];
>            if (text != "") text = text + ": ";
> -          text = text + data.results.slides[idx]["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 />');
> +          if (slide["img"] && OpenLP.showThumbnails)
> +            text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails80x80/") + "'>";
>            var li = $("<li data-icon=\"false\">").append(
> -            $("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text));
> -          if (data.results.slides[idx]["selected"]) {
> +            $("<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;
> @@ -241,6 +251,17 @@
>        }
>      );
>    },
> +  displayThumbnails: function (event) {
> +    event.preventDefault();
> +    var target = $(event.target);
> +    OpenLP.showThumbnails = target.text() == "No" ? false : true;
> +    var dt = new Date();
> +    dt.setTime(dt.getTime() + 365 * 24 * 60 * 60 * 1000);
> +    document.cookie = "displayThumbs=" + OpenLP.showThumbnails + "; expires=" +
> +      dt.toGMTString() + "; path=/";
> +    OpenLP.loadController();
> +    $("#settings").dialog("close");
> +  },
>    search: function (event) {
>      event.preventDefault();
>      var query = OpenLP.escapeString($("#search-text").val())
> @@ -320,12 +341,24 @@
>    },
>    escapeString: function (string) {
>      return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")
> -  }
> +  },
> +  showThumbnails: false
>  }
>  // Initial jQueryMobile options
>  $(document).bind("mobileinit", function(){
>    $.mobile.defaultDialogTransition = "none";
>    $.mobile.defaultPageTransition = "none";
> +  var cookies = document.cookie;
> +  if( cookies )
> +  {
> +    var allcookies = cookies.split(";")
> +    for(ii = 0; ii < allcookies.length; ii++)
> +    {
> +      var parts = allcookies[ii].split("=");
> +      if(parts.length == 2 && parts[0] == "displayThumbs")
> +        OpenLP.showThumbnails = (parts[1]=='true');
> +    }
> +  }
>  });
>  // Service Manager
>  $("#service-manager").live("pagebeforeshow", OpenLP.loadService);
> @@ -345,6 +378,8 @@
>  $("#controller-theme").live("click", OpenLP.themeDisplay);
>  $("#controller-desktop").live("click", OpenLP.desktopDisplay);
>  $("#controller-show").live("click", OpenLP.showDisplay);
> +$("#display-thumbnails").live("click", OpenLP.displayThumbnails);
> +$("#dont-display-thumbnails").live("click", OpenLP.displayThumbnails);
>  // Alerts
>  $("#alert-submit").live("click", OpenLP.showAlert);
>  // Search
> 
> === 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-07-15 13:42:09 +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-07-15 13:42:09 +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,35 @@
>          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']
> +        if not dimensions:
> +            dimensions = ''
> +        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, dimensions)

Not guaranteed to work as the processing is on a thread and may not be ready to be used when needed in 2 lines.

> +                        ext, content_type = self.get_content_type(full_path)
> +                        image = self.image_manager.get_image(full_path, just_file_name, dimensions)
> +                        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.
> @@ -465,11 +496,28 @@
>                          item['tag'] = str(index + 1)
>                      item['text'] = str(frame['text'])
>                      item['html'] = str(frame['html'])
> +                elif current_item.is_image():
> +                    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:
>                      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):
> +                        # If the file is under our app directory tree send the portion after the match
> +                        data_path = AppLocation.get_data_path()
> +                        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
>                  data.append(item)
>          json_data = {'results': {'slides': data}}
>          if current_item:
> 
> === 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-07-15 13:42:09 +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-07-15 13:42:09 +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, '80x80')
> +
> +        # WHEN: the image is retrieved
> +        image = self.image_manager.get_image(full_path, 'church.jpg', '80x80')
> +
> +        # 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, '100x100')
> +
> +        # 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, '80x80')
> +
> +        # 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', '120x120')
> +        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, '')].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-13 20:59:10 +0000
> +++ tests/functional/openlp_core_lib/test_serviceitem.py	2014-07-15 13:42:09 +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'\
> @@ -126,7 +124,7 @@
>  
>          # 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,8 +151,8 @@
>          # 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)
> +        test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
> +        test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
>          frame_array1 = {'path': test_file1, 'title': image_name1}
>          frame_array2 = {'path': test_file2, 'title': image_name2}
>  
> @@ -206,3 +204,41 @@
>                          'This service item should be able to be run in a can be made to Loop')
>          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')
> 
> === 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-07-15 13:42:09 +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):
> +    """
> +    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-07-15 13:42:09 +0000
> @@ -38,8 +38,10 @@
>  
>  from tests.functional import patch, MagicMock
>  from tests.helpers.testmixin import TestMixin
> +from tests.utils.constants import TEST_RESOURCES_PATH
>  
> -from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument
> +from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument,\
> +    _get_text_from_shapes
>  
>  
>  class TestPowerpointController(TestCase, TestMixin):
> @@ -79,7 +81,7 @@
>                           'The name of the presentation controller should be correct')
>  
>  
> -class TestPowerpointDocument(TestCase):
> +class TestPowerpointDocument(TestCase, TestMixin):
>      """
>      Test the PowerpointDocument Class
>      """
> @@ -88,6 +90,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 +107,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,6 +117,8 @@
>          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):
>          """
> @@ -129,3 +140,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 os.name == 'nt' 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 os.name == 'nt' 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 os.name == 'nt' 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-07-15 13:42:09 +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,          #
> @@ -39,6 +39,7 @@
>  
>  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
>  
> @@ -130,7 +131,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,41 +142,40 @@
>              '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'
> @@ -191,17 +191,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 os.name == 'nt':
> +                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-07-15 13:42:09 +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_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-07-15 13:42:09 +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-07-15 13:42:09 +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-07-15 13:42:09 +0000 differ


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


References