This commit is contained in:
Tim Bentley 2014-10-12 13:52:33 +01:00
commit f8fc418925
32 changed files with 1190 additions and 280 deletions

View File

@ -145,11 +145,13 @@ def build_icon(icon):
return button_icon 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. 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 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') log.debug('image_to_byte - start')
byte_array = QtCore.QByteArray() byte_array = QtCore.QByteArray()
@ -158,6 +160,8 @@ def image_to_byte(image):
buffie.open(QtCore.QIODevice.WriteOnly) buffie.open(QtCore.QIODevice.WriteOnly)
image.save(buffie, "PNG") image.save(buffie, "PNG")
log.debug('image_to_byte - end') log.debug('image_to_byte - end')
if not base_64:
return byte_array
# convert to base64 encoding so does not get missed! # convert to base64 encoding so does not get missed!
return bytes(byte_array.toBase64()).decode('utf-8') return bytes(byte_array.toBase64()).decode('utf-8')

View File

@ -106,7 +106,7 @@ class Image(object):
""" """
secondary_priority = 0 secondary_priority = 0
def __init__(self, path, source, background): def __init__(self, path, source, background, width=-1, height=-1):
""" """
Create an image for the :class:`ImageManager`'s cache. Create an image for the :class:`ImageManager`'s cache.
@ -115,7 +115,8 @@ class Image(object):
:class:`~openlp.core.lib.ImageSource` class. :class:`~openlp.core.lib.ImageSource` class.
:param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
ratio does not match with the display ratio. ratio does not match with the display ratio.
:param width: The width of the image, defaults to -1 meaning that the screen width will be used.
:param height: The height of the image, defaults to -1 meaning that the screen height will be used.
""" """
self.path = path self.path = path
self.image = None self.image = None
@ -124,6 +125,8 @@ class Image(object):
self.source = source self.source = source
self.background = background self.background = background
self.timestamp = 0 self.timestamp = 0
self.width = width
self.height = height
# FIXME: We assume that the path exist. The caller has to take care that it exists! # FIXME: We assume that the path exist. The caller has to take care that it exists!
if os.path.exists(path): if os.path.exists(path):
self.timestamp = os.stat(path).st_mtime self.timestamp = os.stat(path).st_mtime
@ -210,13 +213,13 @@ class ImageManager(QtCore.QObject):
image.background = background image.background = background
self._reset_image(image) self._reset_image(image)
def update_image_border(self, path, source, background): def update_image_border(self, path, source, background, width=-1, height=-1):
""" """
Border has changed so update the image affected. Border has changed so update the image affected.
""" """
log.debug('update_image_border') log.debug('update_image_border')
# Mark the image as dirty for a rebuild by setting the image and byte stream to None. # Mark the image as dirty for a rebuild by setting the image and byte stream to None.
image = self._cache[(path, source)] image = self._cache[(path, source, width, height)]
if image.source == source: if image.source == source:
image.background = background image.background = background
self._reset_image(image) self._reset_image(image)
@ -237,12 +240,12 @@ class ImageManager(QtCore.QObject):
if not self.image_thread.isRunning(): if not self.image_thread.isRunning():
self.image_thread.start() self.image_thread.start()
def get_image(self, path, source): def get_image(self, path, source, width=-1, height=-1):
""" """
Return the ``QImage`` from the cache. If not present wait for the background thread to process it. Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
""" """
log.debug('getImage %s' % path) log.debug('getImage %s' % path)
image = self._cache[(path, source)] image = self._cache[(path, source, width, height)]
if image.image is None: if image.image is None:
self._conversion_queue.modify_priority(image, Priority.High) self._conversion_queue.modify_priority(image, Priority.High)
# make sure we are running and if not give it a kick # make sure we are running and if not give it a kick
@ -257,12 +260,12 @@ class ImageManager(QtCore.QObject):
self._conversion_queue.modify_priority(image, Priority.Low) self._conversion_queue.modify_priority(image, Priority.Low)
return image.image return image.image
def get_image_bytes(self, path, source): def get_image_bytes(self, path, source, width=-1, height=-1):
""" """
Returns the byte string for an image. If not present wait for the background thread to process it. 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) log.debug('get_image_bytes %s' % path)
image = self._cache[(path, source)] image = self._cache[(path, source, width, height)]
if image.image_bytes is None: if image.image_bytes is None:
self._conversion_queue.modify_priority(image, Priority.Urgent) self._conversion_queue.modify_priority(image, Priority.Urgent)
# make sure we are running and if not give it a kick # make sure we are running and if not give it a kick
@ -272,14 +275,14 @@ class ImageManager(QtCore.QObject):
time.sleep(0.1) time.sleep(0.1)
return image.image_bytes return image.image_bytes
def add_image(self, path, source, background): def add_image(self, path, source, background, width=-1, height=-1):
""" """
Add image to cache if it is not already there. Add image to cache if it is not already there.
""" """
log.debug('add_image %s' % path) log.debug('add_image %s' % path)
if not (path, source) in self._cache: if not (path, source, width, height) in self._cache:
image = Image(path, source, background) image = Image(path, source, background, width, height)
self._cache[(path, source)] = image self._cache[(path, source, width, height)] = image
self._conversion_queue.put((image.priority, image.secondary_priority, 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. # 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()): for image in list(self._cache.values()):
@ -308,7 +311,10 @@ class ImageManager(QtCore.QObject):
image = self._conversion_queue.get()[2] image = self._conversion_queue.get()[2]
# Generate the QImage for the image. # Generate the QImage for the image.
if image.image is None: 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. # Set the priority to Lowest and stop here as we need to process more important images first.
if image.priority == Priority.Normal: if image.priority == Priority.Normal:
self._conversion_queue.modify_priority(image, Priority.Lowest) self._conversion_queue.modify_priority(image, Priority.Lowest)

View File

@ -39,8 +39,8 @@ import uuid
from PyQt4 import QtGui from PyQt4 import QtGui
from openlp.core.common import RegistryProperties, Settings, translate from openlp.core.common import RegistryProperties, Settings, translate, AppLocation
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -112,7 +112,17 @@ class ItemCapabilities(object):
The capability to edit the title of the item The capability to edit the title of the item
``IsOptical`` ``IsOptical``
.Determines is the service_item is based on an optical device Determines is the service_item is based on an optical device
``HasDisplayTitle``
The item contains 'displaytitle' on every frame which should be
preferred over 'title' when displaying the item
``HasNotes``
The item contains 'notes'
``HasThumbnails``
The item has related thumbnails available
""" """
CanPreview = 1 CanPreview = 1
@ -133,6 +143,9 @@ class ItemCapabilities(object):
CanAutoStartForLive = 16 CanAutoStartForLive = 16
CanEditTitle = 17 CanEditTitle = 17
IsOptical = 18 IsOptical = 18
HasDisplayTitle = 19
HasNotes = 20
HasThumbnails = 21
class ServiceItem(RegistryProperties): class ServiceItem(RegistryProperties):
@ -272,18 +285,22 @@ class ServiceItem(RegistryProperties):
self.raw_footer = [] self.raw_footer = []
self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f]) self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
def add_from_image(self, path, title, background=None): def add_from_image(self, path, title, background=None, thumbnail=None):
""" """
Add an image slide to the service item. Add an image slide to the service item.
:param path: The directory in which the image file is located. :param path: The directory in which the image file is located.
:param title: A title for the slide in the service item. :param title: A title for the slide in the service item.
:param background: :param background:
:param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
""" """
if background: if background:
self.image_border = background self.image_border = background
self.service_item_type = ServiceItemType.Image self.service_item_type = ServiceItemType.Image
self._raw_frames.append({'title': title, 'path': path}) if not thumbnail:
self._raw_frames.append({'title': title, 'path': path})
else:
self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border) self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
self._new_item() self._new_item()
@ -301,16 +318,22 @@ class ServiceItem(RegistryProperties):
self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag}) self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
self._new_item() 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. Add a slide from a command.
:param path: The title of the slide in the service item. :param path: The title of the slide in the service item.
:param file_name: 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 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.service_item_type = ServiceItemType.Command
self._raw_frames.append({'title': file_name, 'image': image, 'path': path}) # If the item should have a display title but this frame doesn't have one, we make one up
if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1)
self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
'display_title': display_title, 'notes': notes})
self._new_item() self._new_item()
def get_service_repr(self, lite_save): def get_service_repr(self, lite_save):
@ -354,7 +377,8 @@ class ServiceItem(RegistryProperties):
service_data = [slide['title'] for slide in self._raw_frames] service_data = [slide['title'] for slide in self._raw_frames]
elif self.service_item_type == ServiceItemType.Command: elif self.service_item_type == ServiceItemType.Command:
for slide in self._raw_frames: for slide in self._raw_frames:
service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']}) service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
'display_title': slide['display_title'], 'notes': slide['notes']})
return {'header': service_header, 'data': service_data} return {'header': service_header, 'data': service_data}
def set_from_service(self, service_item, path=None): def set_from_service(self, service_item, path=None):
@ -425,7 +449,8 @@ class ServiceItem(RegistryProperties):
self.add_from_command(text_image['path'], text_image['title'], text_image['image']) self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
elif path: elif path:
self.has_original_files = False 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: else:
self.add_from_command(text_image['path'], text_image['title'], text_image['image']) self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
self._new_item() self._new_item()

View File

@ -94,8 +94,8 @@ class ListPreviewWidget(QtGui.QTableWidget, RegistryProperties):
Displays the given slide. Displays the given slide.
""" """
self.service_item = service_item self.service_item = service_item
self.clear()
self.setRowCount(0) self.setRowCount(0)
self.clear()
self.setColumnWidth(0, width) self.setColumnWidth(0, width)
row = 0 row = 0
text = [] text = []

View File

@ -1281,7 +1281,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
# Add the children to their parent tree_widget_item. # Add the children to their parent tree_widget_item.
for count, frame in enumerate(service_item_from_item.get_frames()): for count, frame in enumerate(service_item_from_item.get_frames()):
child = QtGui.QTreeWidgetItem(tree_widget_item) child = QtGui.QTreeWidgetItem(tree_widget_item)
text = frame['title'].replace('\n', ' ') # prefer to use a display_title
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
text = frame['display_title'].replace('\n', ' ')
else:
text = frame['title'].replace('\n', ' ')
child.setText(0, text[:40]) child.setText(0, text[:40])
child.setData(0, QtCore.Qt.UserRole, count) child.setData(0, QtCore.Qt.UserRole, count)
if service_item == item_count: if service_item == item_count:

View File

@ -873,6 +873,7 @@ class SlideController(DisplayController, RegistryProperties):
if self.service_item.is_command(): if self.service_item.is_command():
Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
self.update_preview() self.update_preview()
self.selected_row = index
else: else:
self.preview_widget.change_slide(index) self.preview_widget.change_slide(index)
self.slide_selected() self.slide_selected()
@ -1042,8 +1043,8 @@ class SlideController(DisplayController, RegistryProperties):
self.display.image(to_display) self.display.image(to_display)
# reset the store used to display first image # reset the store used to display first image
self.service_item.bg_image_bytes = None self.service_item.bg_image_bytes = None
self.update_preview()
self.selected_row = row self.selected_row = row
self.update_preview()
self.preview_widget.change_slide(row) self.preview_widget.change_slide(row)
self.display.setFocus() self.display.setFocus()
@ -1055,6 +1056,7 @@ class SlideController(DisplayController, RegistryProperties):
""" """
self.preview_widget.change_slide(row) self.preview_widget.change_slide(row)
self.update_preview() self.update_preview()
self.selected_row = row
def update_preview(self): def update_preview(self):
""" """

View File

@ -85,7 +85,7 @@ class BibleFormat(object):
BibleFormat.CSV, BibleFormat.CSV,
BibleFormat.OpenSong, BibleFormat.OpenSong,
BibleFormat.WebDownload, BibleFormat.WebDownload,
BibleFormar.Zefania, BibleFormat.Zefania,
] ]

View File

@ -551,6 +551,7 @@ class ImageMediaItem(MediaManagerItem):
service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.CanLoop)
service_item.add_capability(ItemCapabilities.CanAppend) service_item.add_capability(ItemCapabilities.CanAppend)
service_item.add_capability(ItemCapabilities.CanEditTitle) service_item.add_capability(ItemCapabilities.CanEditTitle)
service_item.add_capability(ItemCapabilities.HasThumbnails)
# force a nonexistent theme # force a nonexistent theme
service_item.theme = -1 service_item.theme = -1
missing_items_file_names = [] missing_items_file_names = []
@ -589,7 +590,7 @@ class ImageMediaItem(MediaManagerItem):
# Continue with the existing images. # Continue with the existing images.
for filename in images_file_names: for filename in images_file_names:
name = os.path.split(filename)[1] name = os.path.split(filename)[1]
service_item.add_from_image(filename, name, background) service_item.add_from_image(filename, name, background, os.path.join(self.service_path, name))
return True return True
def check_group_exists(self, new_group): def check_group_exists(self, new_group):

View File

@ -65,7 +65,7 @@ from PyQt4 import QtCore
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance 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__) log = logging.getLogger(__name__)
@ -257,6 +257,7 @@ class ImpressDocument(PresentationDocument):
self.presentation.Display = ScreenList().current['number'] + 1 self.presentation.Display = ScreenList().current['number'] + 1
self.control = None self.control = None
self.create_thumbnails() self.create_thumbnails()
self.create_titles_and_notes()
return True return True
def create_thumbnails(self): def create_thumbnails(self):
@ -450,22 +451,44 @@ class ImpressDocument(PresentationDocument):
:param slide_no: The slide the notes are required for, starting at 1 :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. Return any text extracted from the presentation page.
:param slide_no: The slide the notes are required for, starting at 1 :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 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 = '' text = ''
pages = self.document.getDrawPages() if TextType.Title <= text_type <= TextType.Notes:
page = pages.getByIndex(slide_no - 1) pages = self.document.getDrawPages()
if notes: if 0 < slide_no <= pages.getCount():
page = page.getNotesPage() page = pages.getByIndex(slide_no - 1)
for index in range(page.getCount()): if text_type == TextType.Notes:
shape = page.getByIndex(index) page = page.getNotesPage()
if shape.supportsService("com.sun.star.drawing.Text"): for index in range(page.getCount()):
text += shape.getString() + '\n' 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 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()
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)

View File

@ -288,13 +288,14 @@ class PresentationMediaItem(MediaManagerItem):
os.path.join(doc.get_temp_folder(), 'mainslide001.png')): os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
doc.load_presentation() doc.load_presentation()
i = 1 i = 1
image_file = 'mainslide%03d.png' % i image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
image = os.path.join(doc.get_temp_folder(), image_file) thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
while os.path.isfile(image): while os.path.isfile(image):
service_item.add_from_image(image, name) service_item.add_from_image(image, name, thumbnail=thumbnail)
i += 1 i += 1
image_file = 'mainslide%03d.png' % i image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i)
image = os.path.join(doc.get_temp_folder(), image_file) thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
service_item.add_capability(ItemCapabilities.HasThumbnails)
doc.close_presentation() doc.close_presentation()
return True return True
else: else:
@ -323,8 +324,21 @@ class PresentationMediaItem(MediaManagerItem):
i = 1 i = 1
img = doc.get_thumbnail_path(i, True) img = doc.get_thumbnail_path(i, True)
if img: if img:
# Get titles and notes
titles, notes = doc.get_titles_and_notes()
service_item.add_capability(ItemCapabilities.HasDisplayTitle)
if notes.count('') != len(notes):
service_item.add_capability(ItemCapabilities.HasNotes)
service_item.add_capability(ItemCapabilities.HasThumbnails)
while img: while img:
service_item.add_from_command(path, name, img) # Use title and note if available
title = ''
if titles and len(titles) >= i:
title = titles[i - 1]
note = ''
if notes and len(notes) >= i:
note = notes[i - 1]
service_item.add_from_command(path, name, img, title, note)
i += 1 i += 1
img = doc.get_thumbnail_path(i, True) img = doc.get_thumbnail_path(i, True)
doc.close_presentation() doc.close_presentation()

View File

@ -27,7 +27,7 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # 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`_ `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
""" """
import os import os
@ -37,16 +37,17 @@ from openlp.core.common import is_win
if is_win(): if is_win():
from win32com.client import Dispatch from win32com.client import Dispatch
import win32com
import winreg import winreg
import win32ui import win32ui
import pywintypes import pywintypes
from openlp.core.lib import ScreenList 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.lib.ui import UiStrings, critical_error_message_box, translate
from openlp.core.common import trace_error_handler from openlp.core.common import trace_error_handler
from .presentationcontroller import PresentationController, PresentationDocument from .presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -136,6 +137,7 @@ class PowerpointDocument(PresentationDocument):
self.controller.process.Presentations.Open(self.file_path, False, False, True) self.controller.process.Presentations.Open(self.file_path, False, False, True)
self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
self.create_thumbnails() self.create_thumbnails()
self.create_titles_and_notes()
# Powerpoint 2013 pops up when loading a file, so we minimize it again # Powerpoint 2013 pops up when loading a file, so we minimize it again
if self.presentation.Application.Version == u'15.0': if self.presentation.Application.Version == u'15.0':
try: try:
@ -392,6 +394,28 @@ class PowerpointDocument(PresentationDocument):
""" """
return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes) return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
def create_titles_and_notes(self):
"""
Writes the list of titles (one per slide)
to 'titles.txt'
and the notes to 'slideNotes[x].txt'
in the thumbnails directory
"""
titles = []
notes = []
for slide in self.presentation.Slides:
try:
text = slide.Shapes.Title.TextFrame.TextRange.Text
except Exception as e:
log.exception(e)
text = ''
titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n')
note = _get_text_from_shapes(slide.NotesPage.Shapes)
if len(note) == 0:
note = ' '
notes.append(note)
self.save_titles_and_notes(titles, notes)
def show_error_msg(self): def show_error_msg(self):
""" """
Stop presentation and display an error message. Stop presentation and display an error message.
@ -410,8 +434,8 @@ def _get_text_from_shapes(shapes):
:param shapes: A set of shapes to search for text. :param shapes: A set of shapes to search for text.
""" """
text = '' text = ''
for index in range(shapes.Count): for shape in shapes:
shape = shapes(index + 1) if shape.PlaceholderFormat.Type == 2: # 2 from is enum PpPlaceholderType.ppPlaceholderBody
if shape.HasTextFrame: if shape.HasTextFrame and shape.TextFrame.HasText:
text += shape.TextFrame.TextRange.Text + '\n' text += shape.TextFrame.TextRange.Text + '\n'
return text return text

View File

@ -29,6 +29,11 @@
import logging import logging
import os import os
import logging
import zipfile
import re
from xml.etree import ElementTree
from openlp.core.common import is_win from openlp.core.common import is_win
@ -127,14 +132,14 @@ class PptviewDocument(PresentationDocument):
temp_folder = self.get_temp_folder() temp_folder = self.get_temp_folder()
size = ScreenList().current['size'] size = ScreenList().current['size']
rect = RECT(size.x(), size.y(), size.right(), size.bottom()) 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') preview_path = os.path.join(temp_folder, 'slide')
# Ensure that the paths are null terminated # 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' preview_path = preview_path.encode('utf-16-le') + b'\0'
if not os.path.isdir(temp_folder): if not os.path.isdir(temp_folder):
os.makedirs(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: if self.ppt_id >= 0:
self.create_thumbnails() self.create_thumbnails()
self.stop_presentation() self.stop_presentation()
@ -154,6 +159,68 @@ class PptviewDocument(PresentationDocument):
path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1)) path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1))
self.convert_thumbnail(path, idx + 1) self.convert_thumbnail(path, idx + 1)
def create_titles_and_notes(self):
"""
Extracts the titles and notes from the zipped file
and writes the list of titles (one per slide)
to 'titles.txt'
and the notes to 'slideNotes[x].txt'
in the thumbnails directory
"""
titles = None
notes = None
filename = os.path.normpath(self.file_path)
# let's make sure we have a valid zipped presentation
if os.path.exists(filename) and zipfile.is_zipfile(filename):
namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
# open the file
with zipfile.ZipFile(filename) as zip_file:
# find the presentation.xml to get the slide count
with zip_file.open('ppt/presentation.xml') as pres:
tree = ElementTree.parse(pres)
nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces)
# initialize the lists
titles = ['' for i in range(len(nodes))]
notes = ['' for i in range(len(nodes))]
# loop thru the file list to find slides and notes
for zip_info in zip_file.infolist():
node_type = ''
index = -1
list_to_add = None
# check if it is a slide
match = re.search("slides/slide(.+)\.xml", zip_info.filename)
if match:
index = int(match.group(1))-1
node_type = 'ctrTitle'
list_to_add = titles
# or a note
match = re.search("notesSlides/notesSlide(.+)\.xml", zip_info.filename)
if match:
index = int(match.group(1))-1
node_type = 'body'
list_to_add = notes
# if it is one of our files, index shouldn't be -1
if index >= 0:
with zip_file.open(zip_info) as zipped_file:
tree = ElementTree.parse(zipped_file)
text = ''
nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t",
namespaces=namespaces)
# if we found any content
if nodes and len(nodes) > 0:
for node in nodes:
if len(text) > 0:
text += '\n'
text += node.text
# Let's remove the \n from the titles and
# just add one at the end
if node_type == 'ctrTitle':
text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n'
list_to_add[index] = text
# now let's write the files
self.save_titles_and_notes(titles, notes)
def close_presentation(self): def close_presentation(self):
""" """
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being

Binary file not shown.

View File

@ -293,6 +293,49 @@ class PresentationDocument(object):
""" """
return '' 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): class PresentationController(object):
""" """
@ -427,3 +470,12 @@ class PresentationController(object):
def close_presentation(self): def close_presentation(self):
pass pass
class TextType(object):
"""
Type Enumeration for Types of Text to request
"""
Title = 0
SlideText = 1
Notes = 2

View File

@ -87,16 +87,30 @@ window.OpenLP = {
var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]"); var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
ul.html(""); ul.html("");
for (idx in data.results.slides) { for (idx in data.results.slides) {
var text = data.results.slides[idx]["tag"]; var indexInt = parseInt(idx,10);
if (text != "") text = text + ": "; var slide = data.results.slides[idx];
text = text + data.results.slides[idx]["text"]; var text = slide["tag"];
if (text != "") {
text = text + ": ";
}
if (slide["title"]) {
text += slide["title"]
} else {
text += slide["text"];
}
if (slide["notes"]) {
text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["notes"] + "</div>");
}
text = text.replace(/\n/g, '<br />'); text = text.replace(/\n/g, '<br />');
var li = $("<li data-icon=\"false\">").append( if (slide["img"]) {
$("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text)); text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails88x88/") + "'>";
if (data.results.slides[idx]["selected"]) { }
var li = $("<li data-icon=\"false\">").append($("<a href=\"#\">").html(text));
if (slide["selected"]) {
li.attr("data-theme", "e"); li.attr("data-theme", "e");
} }
li.children("a").click(OpenLP.setSlide); li.children("a").click(OpenLP.setSlide);
li.find("*").attr("value", indexInt );
ul.append(li); ul.append(li);
} }
OpenLP.currentItem = data.results.item; OpenLP.currentItem = data.results.item;

View File

@ -102,7 +102,21 @@ window.OpenLP = {
$("#verseorder span").removeClass("currenttag"); $("#verseorder span").removeClass("currenttag");
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag"); $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
var slide = OpenLP.currentSlides[OpenLP.currentSlide]; 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 />"); text = text.replace(/\n/g, "<br />");
$("#currentslide").html(text); $("#currentslide").html(text);
text = ""; text = "";
@ -110,7 +124,11 @@ window.OpenLP = {
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) { for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "<p class=\"nextslide\">"; 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]) if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "</p>"; text = text + "</p>";
else else

View File

@ -125,7 +125,7 @@ from mako.template import Template
from PyQt4 import QtCore from PyQt4 import QtCore
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate 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, create_thumb
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FILE_TYPES = { FILE_TYPES = {
@ -159,6 +159,7 @@ class HttpRouter(RegistryProperties):
('^/(stage)$', {'function': self.serve_file, 'secure': False}), ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
('^/(main)$', {'function': self.serve_file, 'secure': False}), ('^/(main)$', {'function': self.serve_file, 'secure': False}),
(r'^/files/(.*)$', {'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'^/api/poll$', {'function': self.poll, 'secure': False}),
(r'^/main/poll$', {'function': self.main_poll, 'secure': False}), (r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
(r'^/main/image$', {'function': self.main_image, 'secure': False}), (r'^/main/image$', {'function': self.main_image, 'secure': False}),
@ -328,7 +329,8 @@ class HttpRouter(RegistryProperties):
'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'no_results': translate('RemotePlugin.Mobile', 'No Results'),
'options': translate('RemotePlugin.Mobile', 'Options'), 'options': translate('RemotePlugin.Mobile', 'Options'),
'service': translate('RemotePlugin.Mobile', 'Service'), '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): def serve_file(self, file_name=None):
@ -380,6 +382,42 @@ class HttpRouter(RegistryProperties):
content_type = FILE_TYPES.get(ext, 'text/plain') content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type return ext, content_type
def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
"""
Serve an image file. If not found return 404.
"""
log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
supported_controllers = ['presentations', 'images']
# -1 means use the default dimension in ImageManager
width = -1
height = -1
if dimensions:
match = re.search('(\d+)x(\d+)', dimensions)
if match:
# let's make sure that the dimensions are within reason
width = sorted([10, int(match.group(1)), 1000])[1]
height = sorted([10, int(match.group(2)), 1000])[1]
content = ''
content_type = None
if controller_name and file_name:
if controller_name in supported_controllers:
full_path = urllib.parse.unquote(file_name)
if '..' not in full_path: # no hacking please
full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
'thumbnails/' + full_path))
if os.path.exists(full_path):
path, just_file_name = os.path.split(full_path)
self.image_manager.add_image(full_path, just_file_name, None, width, height)
ext, content_type = self.get_content_type(full_path)
image = self.image_manager.get_image(full_path, just_file_name, width, height)
content = image_to_byte(image, False)
if len(content) == 0:
return self.do_not_found()
self.send_response(200)
self.send_header('Content-type', content_type)
self.end_headers()
return content
def poll(self): def poll(self):
""" """
Poll OpenLP to determine the current slide number and item name. Poll OpenLP to determine the current slide number and item name.
@ -458,6 +496,7 @@ class HttpRouter(RegistryProperties):
if current_item: if current_item:
for index, frame in enumerate(current_item.get_frames()): for index, frame in enumerate(current_item.get_frames()):
item = {} item = {}
# Handle text (songs, custom, bibles)
if current_item.is_text(): if current_item.is_text():
if frame['verseTag']: if frame['verseTag']:
item['tag'] = str(frame['verseTag']) item['tag'] = str(frame['verseTag'])
@ -465,11 +504,37 @@ class HttpRouter(RegistryProperties):
item['tag'] = str(index + 1) item['tag'] = str(index + 1)
item['text'] = str(frame['text']) item['text'] = str(frame['text'])
item['html'] = str(frame['html']) item['html'] = str(frame['html'])
else: # Handle images, unless a custom thumbnail is given or if thumbnails is disabled
elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'):
item['tag'] = str(index + 1) item['tag'] = str(index + 1)
thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path)
# Create thumbnail if it doesn't exists
if not os.path.exists(full_thumbnail_path):
create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
else:
# Handle presentation etc.
item['tag'] = str(index + 1)
if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
item['title'] = str(frame['display_title'])
if current_item.is_capable(ItemCapabilities.HasNotes):
item['notes'] = str(frame['notes'])
if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
Settings().value('remotes/thumbnails'):
# If the file is under our app directory tree send the portion after the match
data_path = AppLocation.get_data_path()
print(frame)
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
item['text'] = str(frame['title']) item['text'] = str(frame['title'])
item['html'] = str(frame['title']) item['html'] = str(frame['title'])
item['selected'] = (self.live_controller.selected_row == index) item['selected'] = (self.live_controller.selected_row == index)
if current_item.notes:
item['notes'] = item.get('notes', '') + '\n' + current_item.notes
print(item)
data.append(item) data.append(item)
json_data = {'results': {'slides': data}} json_data = {'results': {'slides': data}}
if current_item: if current_item:

View File

@ -144,6 +144,7 @@ class OpenLPServer(RegistryProperties):
try: try:
self.httpd = server_class((address, port), CustomHandler) self.httpd = server_class((address, port), CustomHandler)
log.debug("Server started for class %s %s %d" % (server_class, address, port)) log.debug("Server started for class %s %s %d" % (server_class, address, port))
break
except OSError: except OSError:
log.debug("failed to start http server thread state %d %s" % log.debug("failed to start http server thread state %d %s" %
(loop, self.http_thread.isRunning())) (loop, self.http_thread.isRunning()))
@ -151,6 +152,8 @@ class OpenLPServer(RegistryProperties):
time.sleep(0.1) time.sleep(0.1)
except: except:
log.error('Failed to start server ') log.error('Failed to start server ')
loop += 1
time.sleep(0.1)
def stop_server(self): def stop_server(self):
""" """

View File

@ -62,6 +62,9 @@ class RemoteTab(SettingsTab):
self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
self.twelve_hour_check_box.setObjectName('twelve_hour_check_box') self.twelve_hour_check_box.setObjectName('twelve_hour_check_box')
self.server_settings_layout.addRow(self.twelve_hour_check_box) self.server_settings_layout.addRow(self.twelve_hour_check_box)
self.thumbnails_check_box = QtGui.QCheckBox(self.server_settings_group_box)
self.thumbnails_check_box.setObjectName('thumbnails_check_box')
self.server_settings_layout.addRow(self.thumbnails_check_box)
self.left_layout.addWidget(self.server_settings_group_box) self.left_layout.addWidget(self.server_settings_group_box)
self.http_settings_group_box = QtGui.QGroupBox(self.left_column) self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
self.http_settings_group_box.setObjectName('http_settings_group_box') self.http_settings_group_box.setObjectName('http_settings_group_box')
@ -163,6 +166,7 @@ class RemoteTab(SettingsTab):
self.left_layout.addStretch() self.left_layout.addStretch()
self.right_layout.addStretch() self.right_layout.addStretch()
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls) self.address_edit.textChanged.connect(self.set_urls)
self.port_spin_box.valueChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls)
self.https_port_spin_box.valueChanged.connect(self.set_urls) self.https_port_spin_box.valueChanged.connect(self.set_urls)
@ -176,6 +180,8 @@ class RemoteTab(SettingsTab):
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
'Show thumbnails of non-text slides in remote and stage view.'))
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qr_description_label.setText( self.qr_description_label.setText(
translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/' translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/'
@ -240,6 +246,8 @@ class RemoteTab(SettingsTab):
self.address_edit.setText(Settings().value(self.settings_section + '/ip address')) self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
self.twelve_hour = Settings().value(self.settings_section + '/twelve hour') self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
self.twelve_hour_check_box.setChecked(self.twelve_hour) self.twelve_hour_check_box.setChecked(self.twelve_hour)
self.thumbnails = Settings().value(self.settings_section + '/thumbnails')
self.thumbnails_check_box.setChecked(self.thumbnails)
local_data = AppLocation.get_directory(AppLocation.DataDir) local_data = AppLocation.get_directory(AppLocation.DataDir)
if not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.crt')) or \ if not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.crt')) or \
not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.key')): not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.key')):
@ -271,6 +279,7 @@ class RemoteTab(SettingsTab):
Settings().setValue(self.settings_section + '/https enabled', self.https_settings_group_box.isChecked()) Settings().setValue(self.settings_section + '/https enabled', self.https_settings_group_box.isChecked())
Settings().setValue(self.settings_section + '/ip address', self.address_edit.text()) Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour) Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked()) Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + '/user id', self.user_id.text()) Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text()) Settings().setValue(self.settings_section + '/password', self.password.text())
@ -285,6 +294,15 @@ class RemoteTab(SettingsTab):
if check_state == QtCore.Qt.Checked: if check_state == QtCore.Qt.Checked:
self.twelve_hour = True self.twelve_hour = True
def on_thumbnails_check_box_changed(self, check_state):
"""
Toggle the thumbnail check box.
"""
self.thumbnails = False
# we have a set value convert to True/False
if check_state == QtCore.Qt.Checked:
self.thumbnails = True
def https_changed(self): def https_changed(self):
""" """
Invert the HTTP group box based on Https group settings Invert the HTTP group box based on Https group settings

View File

@ -44,7 +44,8 @@ __default_settings__ = {
'remotes/user id': 'openlp', 'remotes/user id': 'openlp',
'remotes/password': 'password', 'remotes/password': 'password',
'remotes/authentication enabled': False, 'remotes/authentication enabled': False,
'remotes/ip address': '0.0.0.0' 'remotes/ip address': '0.0.0.0',
'remotes/thumbnails': True
} }

View File

@ -162,9 +162,9 @@ class TestAppLocation(TestCase):
patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \ 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.os.path.split') as mocked_split, \
patch('openlp.core.common.applocation.sys') as mocked_sys: 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_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.frozen = 1
mocked_sys.argv = ['openlp'] mocked_sys.argv = ['openlp']
@ -172,7 +172,7 @@ class TestAppLocation(TestCase):
directory = AppLocation.get_directory(AppLocation.PluginsDir) directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN: The correct directory should be returned # 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): def get_frozen_path_in_unfrozen_app_test(self):
""" """

View File

@ -69,16 +69,17 @@ class TestImageManager(TestCase, TestMixin):
Test the Image Manager setup basic functionality Test the Image Manager setup basic functionality
""" """
# GIVEN: the an image add to the image manager # 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 # 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 # THEN returned record is a type of image
self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage') self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
# WHEN: The image bytes are requested. # 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. # THEN: Type should be a str.
self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str') self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str')
@ -89,6 +90,38 @@ class TestImageManager(TestCase, TestMixin):
self.image_manager.get_image(TEST_PATH, 'church1.jpg') self.image_manager.get_image(TEST_PATH, 'church1.jpg')
self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image') self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
def different_dimension_image_test(self):
"""
Test the Image Manager with dimensions
"""
# GIVEN: add an image with specific dimensions
full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
# WHEN: the image is retrieved
image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80)
# THEN: The return should be of type image
self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
# WHEN: adding the same image with different dimensions
self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100)
# THEN: the cache should contain two pictures
self.assertEqual(len(self.image_manager._cache), 2,
'Image manager should consider two dimensions of the same picture as different')
# WHEN: adding the same image with first dimensions
self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
# THEN: the cache should still contain only two pictures
self.assertEqual(len(self.image_manager._cache), 2, 'Same dimensions should not be added again')
# WHEN: calling with correct image, but wrong dimensions
with self.assertRaises(KeyError) as context:
self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension')
def process_cache_test(self): def process_cache_test(self):
""" """
Test the process_cache method Test the process_cache method
@ -151,7 +184,7 @@ class TestImageManager(TestCase, TestMixin):
:param image: The name of the image. E. g. ``image1`` :param image: The name of the image. E. g. ``image1``
""" """
return self.image_manager._cache[(TEST_PATH, image)].priority return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority
def mocked_resize_image(self, *args): def mocked_resize_image(self, *args):
""" """

View File

@ -32,13 +32,11 @@ Package to test the openlp.core.lib package.
import os import os
from unittest import TestCase from unittest import TestCase
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch
from tests.utils import assert_length, convert_file_service_item from tests.utils import assert_length, convert_file_service_item
from openlp.core.common import Registry 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'\ VERSE = 'The Lord said to {r}Noah{/r}: \n'\
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\ 'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
@ -120,13 +118,17 @@ class TestServiceItem(TestCase):
# WHEN: adding an image from a saved Service and mocked exists # WHEN: adding an image from a saved Service and mocked exists
line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj') line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj')
with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\
patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb,\
patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
mocked_get_section_data_path:
mocked_exists.return_value = True mocked_exists.return_value = True
mocked_get_section_data_path.return_value = os.path.normpath('/path/')
service_item.set_from_service(line, TEST_PATH) service_item.set_from_service(line, TEST_PATH)
# THEN: We should get back a valid service item # THEN: We should get back a valid service item
self.assertTrue(service_item.is_valid, 'The new service item should be valid') 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') 'The first frame should match the path to the image')
self.assertEqual(frame_array, service_item.get_frames()[0], self.assertEqual(frame_array, service_item.get_frames()[0],
'The return should match frame array1') 'The return should match frame array1')
@ -153,8 +155,8 @@ class TestServiceItem(TestCase):
# GIVEN: A new service item and a mocked add icon function # GIVEN: A new service item and a mocked add icon function
image_name1 = 'image_1.jpg' image_name1 = 'image_1.jpg'
image_name2 = 'image_2.jpg' image_name2 = 'image_2.jpg'
test_file1 = os.path.join('/home/openlp', image_name1) test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
test_file2 = os.path.join('/home/openlp', image_name2) test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
frame_array1 = {'path': test_file1, 'title': image_name1} frame_array1 = {'path': test_file1, 'title': image_name1}
frame_array2 = {'path': test_file2, 'title': image_name2} frame_array2 = {'path': test_file2, 'title': image_name2}
@ -168,8 +170,12 @@ class TestServiceItem(TestCase):
line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj') line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj')
line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1) line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1)
with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists, \
patch('openlp.core.lib.serviceitem.create_thumb') as mocked_create_thumb, \
patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
mocked_get_section_data_path:
mocked_exists.return_value = True mocked_exists.return_value = True
mocked_get_section_data_path.return_value = os.path.normpath('/path/')
service_item2.set_from_service(line2) service_item2.set_from_service(line2)
service_item.set_from_service(line) service_item.set_from_service(line)
@ -207,6 +213,44 @@ class TestServiceItem(TestCase):
self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend),
'This service item should be able to have new items added to it') 'This service item should be able to have new items added to it')
def add_from_command_for_a_presentation_test(self):
"""
Test the Service Item - adding a presentation
"""
# GIVEN: A service item, a mocked icon and presentation data
service_item = ServiceItem(None)
presentation_name = 'test.pptx'
image = MagicMock()
display_title = 'DisplayTitle'
notes = 'Note1\nNote2\n'
frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH,
'display_title': display_title, 'notes': notes}
# WHEN: adding presentation to service_item
service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes)
# THEN: verify that it is setup as a Command and that the frame data matches
self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
def add_from_comamnd_without_display_title_and_notes_test(self):
"""
Test the Service Item - add from command, but not presentation
"""
# GIVEN: A new service item, a mocked icon and image data
service_item = ServiceItem(None)
image_name = 'test.img'
image = MagicMock()
frame = {'title': image_name, 'image': image, 'path': TEST_PATH,
'display_title': None, 'notes': None}
# WHEN: adding image to service_item
service_item.add_from_command(TEST_PATH, image_name, image)
# THEN: verify that it is setup as a Command and that the frame data matches
self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
def service_item_load_optical_media_from_service_test(self): def service_item_load_optical_media_from_service_test(self):
""" """
Test the Service Item - load an optical media item Test the Service Item - load an optical media item

View File

@ -57,17 +57,18 @@ class TestThemeManager(TestCase):
# GIVEN: A new ThemeManager instance. # GIVEN: A new ThemeManager instance.
theme_manager = ThemeManager() theme_manager = ThemeManager()
theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes') theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes')
zipfile.ZipFile.__init__ = MagicMock() with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
zipfile.ZipFile.__init__.return_value = None patch('zipfile.ZipFile.write') as mocked_zipfile_write:
zipfile.ZipFile.write = MagicMock() mocked_zipfile_init.return_value = None
# WHEN: The theme is exported # WHEN: The theme is exported
theme_manager._export_theme(os.path.join('some', 'path'), 'Default') theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
# THEN: The zipfile should be created at the given path # THEN: The zipfile should be created at the given path
zipfile.ZipFile.__init__.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
zipfile.ZipFile.write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'), mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes',
os.path.join('Default', 'Default.xml')) 'Default', 'Default.xml'),
os.path.join('Default', 'Default.xml'))
def initial_theme_manager_test(self): def initial_theme_manager_test(self):
""" """

View File

@ -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 TestImpressDocument(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

View File

@ -30,16 +30,20 @@
Functional tests to test the PowerPointController class and related methods. Functional tests to test the PowerPointController class and related methods.
""" """
import os import os
if os.name == 'nt':
import pywintypes
import shutil import shutil
from unittest import TestCase from unittest import TestCase
from tempfile import mkdtemp from tempfile import mkdtemp
from tests.functional import patch, MagicMock from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin 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
from openlp.core.common import is_win
if is_win():
import pywintypes
class TestPowerpointController(TestCase, TestMixin): class TestPowerpointController(TestCase, TestMixin):
@ -79,7 +83,7 @@ class TestPowerpointController(TestCase, TestMixin):
'The name of the presentation controller should be correct') 'The name of the presentation controller should be correct')
class TestPowerpointDocument(TestCase): class TestPowerpointDocument(TestCase, TestMixin):
""" """
Test the PowerpointDocument Class Test the PowerpointDocument Class
""" """
@ -88,6 +92,11 @@ class TestPowerpointDocument(TestCase):
""" """
Set up the patches and mocks need for all tests. 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( self.powerpoint_document_stop_presentation_patcher = patch(
'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation') 'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation')
self.presentation_document_get_temp_folder_patcher = patch( self.presentation_document_get_temp_folder_patcher = patch(
@ -100,6 +109,8 @@ class TestPowerpointDocument(TestCase):
self.mock_controller = MagicMock() self.mock_controller = MagicMock()
self.mock_presentation = MagicMock() self.mock_presentation = MagicMock()
self.mock_presentation_document_get_temp_folder.return_value = 'temp folder' 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): def tearDown(self):
""" """
@ -108,12 +119,14 @@ class TestPowerpointDocument(TestCase):
self.powerpoint_document_stop_presentation_patcher.stop() self.powerpoint_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop() self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop() self.presentation_document_setup_patcher.stop()
self.destroy_settings()
shutil.rmtree(self.temp_folder)
def show_error_msg_test(self): def show_error_msg_test(self):
""" """
Test the PowerpointDocument.show_error_msg() method gets called on com exception Test the PowerpointDocument.show_error_msg() method gets called on com exception
""" """
if os.name == 'nt': if is_win():
# GIVEN: A PowerpointDocument with mocked controller and presentation # GIVEN: A PowerpointDocument with mocked controller and presentation
with patch('openlp.plugins.presentations.lib.powerpointcontroller.critical_error_message_box') as \ with patch('openlp.plugins.presentations.lib.powerpointcontroller.critical_error_message_box') as \
mocked_critical_error_message_box: mocked_critical_error_message_box:
@ -129,3 +142,95 @@ class TestPowerpointDocument(TestCase):
'integration and the presentation will be stopped.' 'integration and the presentation will be stopped.'
' Restart the presentation if you wish to ' ' Restart the presentation if you wish to '
'present it.') 'present it.')
# add _test to the following if necessary
def verify_loading_document(self):
"""
Test loading a document in PowerPoint
"""
if is_win() and self.real_controller.check_available():
# GIVEN: A PowerpointDocument and a presentation
doc = PowerpointDocument(self.real_controller, self.file_name)
# WHEN: loading the filename
doc.load_presentation()
result = doc.is_loaded()
# THEN: result should be true
self.assertEqual(result, True, 'The result should be True')
else:
self.skipTest('Powerpoint not available, skipping test.')
def create_titles_and_notes_test(self):
"""
Test creating the titles from PowerPoint
"""
if is_win() and self.real_controller.check_available():
# GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
self.doc = PowerpointDocument(self.real_controller, self.file_name)
self.doc.save_titles_and_notes = MagicMock()
self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
slide = MagicMock()
slide.Shapes.Title.TextFrame.TextRange.Text = 'SlideText'
pres = MagicMock()
pres.Slides = [slide, slide]
self.doc.presentation = pres
# WHEN reading the titles and notes
self.doc.create_titles_and_notes()
# THEN the save should have been called exactly once with 2 titles and 2 notes
self.doc.save_titles_and_notes.assert_called_once_with(['SlideText\n', 'SlideText\n'], [' ', ' '])
else:
self.skipTest('Powerpoint not available, skipping test.')
def create_titles_and_notes_with_no_slides_test(self):
"""
Test creating the titles from PowerPoint when it returns no slides
"""
if is_win() and self.real_controller.check_available():
# GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
doc = PowerpointDocument(self.real_controller, self.file_name)
doc.save_titles_and_notes = MagicMock()
doc._PowerpointDocument__get_text_from_shapes = MagicMock()
pres = MagicMock()
pres.Slides = []
doc.presentation = pres
# WHEN reading the titles and notes
doc.create_titles_and_notes()
# THEN the save should have been called exactly once with empty titles and notes
doc.save_titles_and_notes.assert_called_once_with([], [])
else:
self.skipTest('Powerpoint not available, skipping test.')
def get_text_from_shapes_test(self):
"""
Test getting text from powerpoint shapes
"""
# GIVEN: mocked shapes
shape = MagicMock()
shape.PlaceholderFormat.Type = 2
shape.HasTextFrame = shape.TextFrame.HasText = True
shape.TextFrame.TextRange.Text = 'slideText'
shapes = [shape, shape]
# WHEN: getting the text
result = _get_text_from_shapes(shapes)
# THEN: it should return the text
self.assertEqual(result, 'slideText\nslideText\n', 'result should match \'slideText\nslideText\n\'')
def get_text_from_shapes_with_no_shapes_test(self):
"""
Test getting text from powerpoint shapes with no shapes
"""
# GIVEN: empty shapes array
shapes = []
# WHEN: getting the text
result = _get_text_from_shapes(shapes)
# THEN: it should not fail but return empty string
self.assertEqual(result, '', 'result should be empty')

View File

@ -4,8 +4,8 @@
############################################################################### ###############################################################################
# OpenLP - Open Source Lyrics Projection # # OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman # # Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # # Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # # Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
@ -31,16 +31,19 @@ This module contains tests for the pptviewcontroller module of the Presentations
""" """
import os import os
import shutil import shutil
if os.name == 'nt':
from ctypes import cdll
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
from tests.utils.constants import TEST_RESOURCES_PATH
from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
from openlp.core.common import is_win
if is_win():
from ctypes import cdll
class TestPptviewController(TestCase, TestMixin): class TestPptviewController(TestCase, TestMixin):
@ -98,7 +101,7 @@ class TestPptviewController(TestCase, TestMixin):
available = controller.check_available() available = controller.check_available()
# THEN: On windows it should return True, on other platforms False # THEN: On windows it should return True, on other platforms False
if os.name == 'nt': if is_win():
self.assertTrue(available, 'check_available should return True on windows.') self.assertTrue(available, 'check_available should return True on windows.')
else: else:
self.assertFalse(available, 'check_available should return False when not on windows.') self.assertFalse(available, 'check_available should return False when not on windows.')
@ -130,7 +133,7 @@ class TestPptviewDocument(TestCase):
""" """
Set up the patches and mocks need for all tests. 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( self.pptview_document_create_thumbnails_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails') 'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
self.pptview_document_stop_presentation_patcher = patch( self.pptview_document_stop_presentation_patcher = patch(
@ -141,46 +144,45 @@ class TestPptviewDocument(TestCase):
'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup') 'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList') self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
self.rect_patcher = MagicMock() self.rect_patcher = MagicMock()
self.mock_os_isdir = self.os_isdir_patcher.start()
self.mock_os = self.os_patcher.start()
self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_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_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_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start() self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
self.mock_rect = self.rect_patcher.start() self.mock_rect = self.rect_patcher.start()
self.mock_screen_list = self.screen_list_patcher.start() self.mock_screen_list = self.screen_list_patcher.start()
self.mock_controller = MagicMock() self.mock_controller = MagicMock()
self.mock_presentation = MagicMock() self.mock_presentation = MagicMock()
self.temp_folder = mkdtemp()
self.mock_presentation_document_get_temp_folder.return_value = 'temp folder' self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
def tearDown(self): def tearDown(self):
""" """
Stop the patches Stop the patches
""" """
self.os_patcher.stop() self.os_isdir_patcher.stop()
self.pptview_document_create_thumbnails_patcher.stop() self.pptview_document_create_thumbnails_patcher.stop()
self.pptview_document_stop_presentation_patcher.stop() self.pptview_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop() self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop() self.presentation_document_setup_patcher.stop()
self.rect_patcher.stop() self.rect_patcher.stop()
self.screen_list_patcher.stop() self.screen_list_patcher.stop()
shutil.rmtree(self.temp_folder)
def load_presentation_succesfull_test(self): def load_presentation_succesfull_test(self):
""" """
Test the PptviewDocument.load_presentation() method when the PPT is successfully opened Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
""" """
# GIVEN: A reset mocked_os # 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) # 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 self.mock_controller.process.OpenPPT.return_value = 0
instance = PptviewDocument(self.mock_controller, self.mock_presentation) instance = PptviewDocument(self.mock_controller, self.mock_presentation)
instance.file_path = 'test\path.ppt' instance.file_path = 'test\path.ppt'
if os.name == 'nt': if is_win():
result = instance.load_presentation() result = instance.load_presentation()
# THEN: PptviewDocument.load_presentation should return True # THEN: PptviewDocument.load_presentation should return True
@ -191,17 +193,78 @@ class TestPptviewDocument(TestCase):
Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
not successfully opened not successfully opened
""" """
# GIVEN: A reset mocked_os # GIVEN: A reset mock_os_isdir
self.mock_os.reset() self.mock_os_isdir.reset()
# WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1) # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
self.mock_os.path.isdir.return_value = False with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs:
self.mock_controller.process.OpenPPT.return_value = -1 self.mock_os_isdir.return_value = False
instance = PptviewDocument(self.mock_controller, self.mock_presentation) self.mock_controller.process.OpenPPT.return_value = -1
instance.file_path = 'test\path.ppt' instance = PptviewDocument(self.mock_controller, self.mock_presentation)
if os.name == 'nt': instance.file_path = 'test\path.ppt'
result = instance.load_presentation() if is_win():
result = instance.load_presentation()
# THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
self.mock_os.makedirs.assert_called_once_with('temp folder') mock_makedirs.assert_called_once_with(self.temp_folder)
self.assertFalse(result) 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')

View File

@ -1,158 +1,166 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
############################################################################### ###############################################################################
# OpenLP - Open Source Lyrics Projection # # OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman # # Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # # Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # # Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # # Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # # Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # # Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it # # 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 # # under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. # # Software Foundation; version 2 of the License. #
# # # #
# This program is distributed in the hope that it will be useful, but WITHOUT # # This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. # # more details. #
# # # #
# You should have received a copy of the GNU General Public License along # # 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 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
This module contains tests for the Presentation Controller. Functional tests to test the PresentationController and PresentationDocument
""" classes and related methods.
from unittest import TestCase """
from unittest import TestCase
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument import os
from tests.functional import MagicMock, patch from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
from tests.functional import MagicMock, patch, mock_open
class TestPresentationController(TestCase): FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder'
"""
Test the PresentationController.
""" class TestPresentationController(TestCase):
# TODO: Items left to test """
# PresentationController Test the PresentationController.
# __init__ """
# enabled def setUp(self):
# is_available mocked_plugin = MagicMock()
# check_available mocked_plugin.settings_section = 'presentations'
# start_process self.presentation = PresentationController(mocked_plugin)
# kill self.document = PresentationDocument(self.presentation, '')
# add_document
# remove_doc def constructor_test(self):
# close_presentation """
# _get_plugin_manager Test the Constructor
"""
def constructor_test(self): # GIVEN: A mocked plugin
"""
Test the Constructor # WHEN: The PresentationController is created
"""
# GIVEN: No presentation controller # THEN: The name of the presentation controller should be correct
controller = None self.assertEqual('PresentationController', self.presentation.name,
'The name of the presentation controller should be correct')
# WHEN: The presentation controller object is created
mock_plugin = MagicMock() def save_titles_and_notes_test(self):
mock_plugin.settings_section = '' """
controller = PresentationController(plugin=mock_plugin) Test PresentationDocument.save_titles_and_notes method with two valid lists
"""
# THEN: The name of the presentation controller should be correct # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
self.assertEqual('PresentationController', controller.name, mocked_open = mock_open()
'The name of the presentation controller should be correct') with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
titles = ['uno', 'dos']
notes = ['one', 'two']
class TestPresentationDocument(TestCase):
""" # WHEN: calling save_titles_and_notes
Test the PresentationDocument Class mocked_get_thumbnail_folder.return_value = 'test'
""" self.document.save_titles_and_notes(titles, notes)
# TODO: Items left to test
# PresentationDocument # THEN: the last call to open should have been for slideNotes2.txt
# __init__ mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w')
# load_presentation mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w')
# presentation_deleted mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w')
# get_file_name self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
# get_thumbnail_folder mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
# get_temp_folder mocked_open().write.assert_called_any('one')
# check_thumbnails mocked_open().write.assert_called_any('two')
# close_presentation
# is_active def save_titles_and_notes_with_None_test(self):
# is_loaded """
# blank_screen Test PresentationDocument.save_titles_and_notes method with no data
# unblank_screen """
# is_blank # GIVEN: None and an empty list and a mocked open and get_thumbnail_folder
# stop_presentation with patch('builtins.open') as mocked_open, patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
# start_presentation titles = None
# get_slide_number notes = None
# get_slide_count
# goto_slide # WHEN: calling save_titles_and_notes
# next_step mocked_get_thumbnail_folder.return_value = 'test'
# previous_step self.document.save_titles_and_notes(titles, notes)
# convert_thumbnail
# get_thumbnail_path # THEN: No file should have been created
# poll_slidenumber self.assertEqual(mocked_open.call_count, 0, 'No file should be created')
# get_slide_text
# get_slide_notes def get_titles_and_notes_test(self):
"""
def setUp(self): Test PresentationDocument.get_titles_and_notes method
""" """
Set up the patches and mocks need for all tests. # GIVEN: A mocked open, get_thumbnail_folder and exists
"""
self.check_directory_exists_patcher = \ with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \
patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder') mocked_get_thumbnail_folder.return_value = 'test'
self._setup_patcher = \ mocked_exists.return_value = True
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
# WHEN: calling get_titles_and_notes
self.mock_check_directory_exists = self.check_directory_exists_patcher.start() result_titles, result_notes = self.document.get_titles_and_notes()
self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
self.mock_setup = self._setup_patcher.start() # 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.mock_controller = MagicMock() 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.mock_get_thumbnail_folder.return_value = 'returned/path/' 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')
def tearDown(self): mocked_open.assert_any_call(os.path.join('test', 'titles.txt'))
""" mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'))
Stop the patches mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'))
""" self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
self.check_directory_exists_patcher.stop()
self.get_thumbnail_folder_patcher.stop() def get_titles_and_notes_with_file_not_found_test(self):
self._setup_patcher.stop() """
Test PresentationDocument.get_titles_and_notes method with file not found
def initialise_presentation_document_test(self): """
""" # GIVEN: A mocked open, get_thumbnail_folder and exists
Test the PresentationDocument __init__ method when initialising the PresentationDocument Class with patch('builtins.open') as mocked_open, \
""" patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
# GIVEN: A reset mock_setup and mocked controller patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
self.mock_setup.reset() mocked_get_thumbnail_folder.return_value = 'test'
mocked_exists.return_value = False
# WHEN: Creating an instance of PresentationDocument
PresentationDocument(self.mock_controller, 'Name') # WHEN: calling get_titles_and_notes
result_titles, result_notes = self.document.get_titles_and_notes()
# THEN: PresentationDocument.__init__ should have been called with the correct arguments
self.mock_setup.assert_called_once_with('Name') # THEN: it should return two empty lists
self.assertIs(type(result_titles), list, 'result_titles should be of type list')
def presentation_document_setup_test(self): self.assertEqual(len(result_titles), 0, 'there be no titles')
""" self.assertIs(type(result_notes), list, 'result_notes should be a list')
Test the PresentationDocument _setup method when initialising the PresentationDocument Class 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._setup_patcher.stop() self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
# GIVEN: A mocked controller, patched check_directory_exists_patcher and patched get_thumbnail_folder method def get_titles_and_notes_with_file_error_test(self):
"""
# WHEN: Creating an instance of PresentationDocument Test PresentationDocument.get_titles_and_notes method with file errors
PresentationDocument(self.mock_controller, 'Name') """
# GIVEN: A mocked open, get_thumbnail_folder and exists
# THEN: check_directory_exists should have been called with the correct arguments with patch('builtins.open') as mocked_open, \
self.mock_check_directory_exists.assert_called_once_with('returned/path/') patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
self._setup_patcher.start() 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')

View File

@ -48,7 +48,8 @@ __default_settings__ = {
'remotes/user id': 'openlp', 'remotes/user id': 'openlp',
'remotes/password': 'password', 'remotes/password': 'password',
'remotes/authentication enabled': False, 'remotes/authentication enabled': False,
'remotes/ip address': '0.0.0.0' 'remotes/ip address': '0.0.0.0',
'remotes/thumbnails': True
} }
ZERO_URL = '0.0.0.0' ZERO_URL = '0.0.0.0'
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources')) TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))

View File

@ -30,10 +30,12 @@
This module contains tests for the lib submodule of the Remotes plugin. This module contains tests for the lib submodule of the Remotes plugin.
""" """
import os import os
import urllib.request
from unittest import TestCase from unittest import TestCase
from openlp.core.common import Settings, Registry from openlp.core.common import Settings, Registry
from openlp.plugins.remotes.lib.httpserver import HttpRouter from openlp.plugins.remotes.lib.httpserver import HttpRouter
from urllib.parse import urlparse
from tests.functional import MagicMock, patch, mock_open from tests.functional import MagicMock, patch, mock_open
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -186,3 +188,86 @@ class TestRouter(TestCase, TestMixin):
self.router.send_response.assert_called_once_with(200) self.router.send_response.assert_called_once_with(200)
self.router.send_header.assert_called_once_with('Content-type', 'text/html') 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') 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')

Binary file not shown.

Binary file not shown.