openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #24047
[Merge] lp:~tomasgroth/openlp/better-remote into lp:openlp
Tomas Groth has proposed merging lp:~tomasgroth/openlp/better-remote into lp:openlp.
Requested reviews:
Tim Bentley (trb143)
Raoul Snyman (raoul-snyman)
For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/226795
Continuation of Felipes work.
Changes to remote control:
- Displays the title of the slide (presentations)
- Displays the presenter's notes (presentations)
- Displays a thumbnail for each slide (presentations, images)
- Added settings page for the remote to choose if thumbnails are displayed (presentations)
- defaults to no display
- persisted on cookies
- Fixed bug that was preventing the remote to be updated with correct slide (presentations)
- Added thumbnails and notes to stage view when available
- Displays the service notes (general)
Changes to the main display:
- Display the title of the slide on each item on the slide controller (presentations)
--
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/226795
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 2014-05-01 17:49:43 +0000
+++ openlp/core/lib/__init__.py 2014-07-15 10:08:10 +0000
@@ -145,11 +145,13 @@
return button_icon
-def image_to_byte(image):
+def image_to_byte(image, base_64=True):
"""
Resize an image to fit on the current screen for the web and returns it as a byte stream.
:param image: The image to converted.
+ :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
+ To preserve original intention, this defaults to True
"""
log.debug('image_to_byte - start')
byte_array = QtCore.QByteArray()
@@ -158,6 +160,8 @@
buffie.open(QtCore.QIODevice.WriteOnly)
image.save(buffie, "PNG")
log.debug('image_to_byte - end')
+ if not base_64:
+ return byte_array
# convert to base64 encoding so does not get missed!
return bytes(byte_array.toBase64()).decode('utf-8')
=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py 2014-03-20 19:10:31 +0000
+++ openlp/core/lib/imagemanager.py 2014-07-15 10:08:10 +0000
@@ -36,6 +36,7 @@
import os
import time
import queue
+import re
from PyQt4 import QtCore
@@ -106,7 +107,7 @@
"""
secondary_priority = 0
- def __init__(self, path, source, background):
+ def __init__(self, path, source, background, dimensions=''):
"""
Create an image for the :class:`ImageManager`'s cache.
@@ -124,6 +125,15 @@
self.source = source
self.background = background
self.timestamp = 0
+ match = re.search('(\d+)x(\d+)', dimensions)
+ if match:
+ # let's make sure that the dimensions are within reason
+ self.width = sorted([10, int(match.group(1)), 1000])[1]
+ self.height = sorted([10, int(match.group(2)), 1000])[1]
+ else:
+ # -1 means use the default dimension in ImageManager
+ self.width = -1
+ self.height = -1
# FIXME: We assume that the path exist. The caller has to take care that it exists!
if os.path.exists(path):
self.timestamp = os.stat(path).st_mtime
@@ -210,13 +220,13 @@
image.background = background
self._reset_image(image)
- def update_image_border(self, path, source, background):
+ def update_image_border(self, path, source, background, dimensions=''):
"""
Border has changed so update the image affected.
"""
log.debug('update_image_border')
# Mark the image as dirty for a rebuild by setting the image and byte stream to None.
- image = self._cache[(path, source)]
+ image = self._cache[(path, source, dimensions)]
if image.source == source:
image.background = background
self._reset_image(image)
@@ -237,12 +247,12 @@
if not self.image_thread.isRunning():
self.image_thread.start()
- def get_image(self, path, source):
+ def get_image(self, path, source, dimensions=''):
"""
Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
"""
log.debug('getImage %s' % path)
- image = self._cache[(path, source)]
+ image = self._cache[(path, source, dimensions)]
if image.image is None:
self._conversion_queue.modify_priority(image, Priority.High)
# make sure we are running and if not give it a kick
@@ -257,12 +267,12 @@
self._conversion_queue.modify_priority(image, Priority.Low)
return image.image
- def get_image_bytes(self, path, source):
+ def get_image_bytes(self, path, source, dimensions=''):
"""
Returns the byte string for an image. If not present wait for the background thread to process it.
"""
log.debug('get_image_bytes %s' % path)
- image = self._cache[(path, source)]
+ image = self._cache[(path, source, dimensions)]
if image.image_bytes is None:
self._conversion_queue.modify_priority(image, Priority.Urgent)
# make sure we are running and if not give it a kick
@@ -272,14 +282,14 @@
time.sleep(0.1)
return image.image_bytes
- def add_image(self, path, source, background):
+ def add_image(self, path, source, background, dimensions=''):
"""
Add image to cache if it is not already there.
"""
log.debug('add_image %s' % path)
- if not (path, source) in self._cache:
- image = Image(path, source, background)
- self._cache[(path, source)] = image
+ if not (path, source, dimensions) in self._cache:
+ image = Image(path, source, background, dimensions)
+ self._cache[(path, source, dimensions)] = image
self._conversion_queue.put((image.priority, image.secondary_priority, image))
# Check if the there are any images with the same path and check if the timestamp has changed.
for image in list(self._cache.values()):
@@ -308,7 +318,10 @@
image = self._conversion_queue.get()[2]
# Generate the QImage for the image.
if image.image is None:
- image.image = resize_image(image.path, self.width, self.height, image.background)
+ # Let's see if the image was requested with specific dimensions
+ width = self.width if image.width == -1 else image.width
+ height = self.height if image.height == -1 else image.height
+ image.image = resize_image(image.path, width, height, image.background)
# Set the priority to Lowest and stop here as we need to process more important images first.
if image.priority == Priority.Normal:
self._conversion_queue.modify_priority(image, Priority.Lowest)
=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py 2014-04-14 18:28:04 +0000
+++ openlp/core/lib/serviceitem.py 2014-07-15 10:08:10 +0000
@@ -111,6 +111,16 @@
``CanEditTitle``
The capability to edit the title of the item
+ ``HasDisplayTitle``
+ The item contains 'displaytitle' on every frame which should be
+ preferred over 'title' when displaying the item
+
+ ``HasNotes``
+ The item contains 'notes'
+
+ ``HasThumbnails``
+ The item has related thumbnails available
+
"""
CanPreview = 1
CanEdit = 2
@@ -129,6 +139,9 @@
HasBackgroundAudio = 15
CanAutoStartForLive = 16
CanEditTitle = 17
+ HasDisplayTitle = 18
+ HasNotes = 19
+ HasThumbnails = 20
class ServiceItem(RegistryProperties):
@@ -297,16 +310,19 @@
self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag})
self._new_item()
- def add_from_command(self, path, file_name, image):
+ def add_from_command(self, path, file_name, image, display_title=None, notes=None):
"""
Add a slide from a command.
:param path: The title of the slide in the service item.
:param file_name: The title of the slide in the service item.
:param image: The command of/for the slide.
+ :param display_title: Title to show in gui/webinterface, optional.
+ :param notes: Notes to show in the webinteface, optional.
"""
self.service_item_type = ServiceItemType.Command
- self._raw_frames.append({'title': file_name, 'image': image, 'path': path})
+ self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
+ 'display_title': display_title, 'notes': notes})
self._new_item()
def get_service_repr(self, lite_save):
@@ -350,7 +366,8 @@
service_data = [slide['title'] for slide in self._raw_frames]
elif self.service_item_type == ServiceItemType.Command:
for slide in self._raw_frames:
- service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']})
+ service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'],
+ 'display_title': slide['display_title'], 'notes': slide['notes']})
return {'header': service_header, 'data': service_data}
def set_from_service(self, service_item, path=None):
@@ -418,7 +435,8 @@
self.title = text_image['title']
if path:
self.has_original_files = False
- self.add_from_command(path, text_image['title'], text_image['image'])
+ self.add_from_command(path, text_image['title'], text_image['image'],
+ text_image.get('display_title', ''), text_image.get('notes', ''))
else:
self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
self._new_item()
=== modified file 'openlp/core/ui/listpreviewwidget.py'
--- openlp/core/ui/listpreviewwidget.py 2014-05-19 19:19:05 +0000
+++ openlp/core/ui/listpreviewwidget.py 2014-07-15 10:08:10 +0000
@@ -94,8 +94,8 @@
Displays the given slide.
"""
self.service_item = service_item
+ self.setRowCount(0)
self.clear()
- self.setRowCount(0)
self.setColumnWidth(0, width)
row = 0
text = []
=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py 2014-07-07 11:17:24 +0000
+++ openlp/core/ui/servicemanager.py 2014-07-15 10:08:10 +0000
@@ -1281,7 +1281,14 @@
# Add the children to their parent tree_widget_item.
for count, frame in enumerate(service_item_from_item.get_frames()):
child = QtGui.QTreeWidgetItem(tree_widget_item)
- text = frame['title'].replace('\n', ' ')
+ # prefer to use a display_title
+ if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
+ text = frame['display_title'].replace('\n', ' ')
+ # oops, it is missing, let's make one up
+ if len(text.strip()) == 0:
+ text = '[slide ' + str(count+1) + ']'
+ else:
+ text = frame['title'].replace('\n', ' ')
child.setText(0, text[:40])
child.setData(0, QtCore.Qt.UserRole, count)
if service_item == item_count:
=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py 2014-07-12 23:47:53 +0000
+++ openlp/core/ui/slidecontroller.py 2014-07-15 10:08:10 +0000
@@ -867,12 +867,17 @@
:param message: remote message to be processed.
"""
- index = int(message[0])
+ index = 0
+ if len(message) == 0 or message[0] == 'undefined':
+ return
+ else:
+ index = int(message[0])
if not self.service_item:
return
if self.service_item.is_command():
Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
self.update_preview()
+ self.selected_row = index
else:
self.preview_widget.change_slide(index)
self.slide_selected()
@@ -1042,8 +1047,8 @@
self.display.image(to_display)
# reset the store used to display first image
self.service_item.bg_image_bytes = None
- self.update_preview()
self.selected_row = row
+ self.update_preview()
self.preview_widget.change_slide(row)
self.display.setFocus()
@@ -1055,6 +1060,7 @@
"""
self.preview_widget.change_slide(row)
self.update_preview()
+ self.selected_row = row
def update_preview(self):
"""
=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py 2014-04-24 02:57:02 +0000
+++ openlp/plugins/images/lib/mediaitem.py 2014-07-15 10:08:10 +0000
@@ -551,6 +551,7 @@
service_item.add_capability(ItemCapabilities.CanLoop)
service_item.add_capability(ItemCapabilities.CanAppend)
service_item.add_capability(ItemCapabilities.CanEditTitle)
+ service_item.add_capability(ItemCapabilities.HasThumbnails)
# force a nonexistent theme
service_item.theme = -1
missing_items_file_names = []
=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py 2014-04-12 20:19:22 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py 2014-07-15 10:08:10 +0000
@@ -63,7 +63,7 @@
from openlp.core.lib import ScreenList
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
-from .presentationcontroller import PresentationController, PresentationDocument
+from .presentationcontroller import PresentationController, PresentationDocument, TextType
log = logging.getLogger(__name__)
@@ -255,6 +255,7 @@
self.presentation.Display = ScreenList().current['number'] + 1
self.control = None
self.create_thumbnails()
+ self.create_titles_and_notes()
return True
def create_thumbnails(self):
@@ -448,22 +449,47 @@
:param slide_no: The slide the notes are required for, starting at 1
"""
- return self.__get_text_from_page(slide_no, True)
+ return self.__get_text_from_page(slide_no, TextType.Notes)
- def __get_text_from_page(self, slide_no, notes=False):
+ def __get_text_from_page(self, slide_no, text_type=TextType.SlideText):
"""
Return any text extracted from the presentation page.
:param slide_no: The slide the notes are required for, starting at 1
:param notes: A boolean. If set the method searches the notes of the slide.
+ :param text_type: A TextType. Enumeration of the types of supported text.
"""
text = ''
+ if TextType.Title <= text_type <= TextType.Notes:
+ pages = self.document.getDrawPages()
+ if 0 < slide_no <= pages.getCount():
+ page = pages.getByIndex(slide_no - 1)
+ if text_type == TextType.Notes:
+ page = page.getNotesPage()
+ for index in range(page.getCount()):
+ shape = page.getByIndex(index)
+ shape_type = shape.getShapeType()
+ if shape.supportsService("com.sun.star.drawing.Text"):
+ # if they requested title, make sure it is the title
+ if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape":
+ text += shape.getString() + '\n'
+ return text
+
+ def create_titles_and_notes(self):
+ """
+ Writes the list of titles (one per slide)
+ to 'titles.txt'
+ and the notes to 'slideNotes[x].txt'
+ in the thumbnails directory
+ """
+ titles = []
+ notes = []
pages = self.document.getDrawPages()
- page = pages.getByIndex(slide_no - 1)
- if notes:
- page = page.getNotesPage()
- for index in range(page.getCount()):
- shape = page.getByIndex(index)
- if shape.supportsService("com.sun.star.drawing.Text"):
- text += shape.getString() + '\n'
- return text
+ for slide_no in range(1, pages.getCount() + 1):
+ titles.append(self.__get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
+ note = self.__get_text_from_page(slide_no, TextType.Notes)
+ if len(note) == 0:
+ note = ' '
+ notes.append(note)
+ self.save_titles_and_notes(titles, notes)
+ return
=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py 2014-06-05 16:25:37 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py 2014-07-15 10:08:10 +0000
@@ -295,6 +295,7 @@
i += 1
image_file = 'mainslide%03d.png' % i
image = os.path.join(doc.get_temp_folder(), image_file)
+ service_item.add_capability(ItemCapabilities.HasThumbnails)
doc.close_presentation()
return True
else:
@@ -323,11 +324,25 @@
i = 1
img = doc.get_thumbnail_path(i, True)
if img:
+ # Get titles and notes
+ titles, notes = doc.get_titles_and_notes()
while img:
- service_item.add_from_command(path, name, img)
+ # Use title and note if available
+ title = None
+ if titles and len(titles) >= i:
+ title = titles[i - 1]
+ note = None
+ if notes and len(notes) >= i:
+ note = notes[i - 1]
+ service_item.add_from_command(path, name, img, title, note)
i += 1
img = doc.get_thumbnail_path(i, True)
doc.close_presentation()
+ if titles.count('') != len(titles):
+ service_item.add_capability(ItemCapabilities.HasDisplayTitle)
+ if notes.count('') != len(notes):
+ service_item.add_capability(ItemCapabilities.HasNotes)
+ service_item.add_capability(ItemCapabilities.HasThumbnails)
return True
else:
# File is no longer present
=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py 2014-07-03 11:21:12 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py 2014-07-15 10:08:10 +0000
@@ -27,7 +27,7 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
-This modul is for controlling powerpiont. PPT API documentation:
+This module is for controlling powerpoint. PPT API documentation:
`http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
"""
import os
@@ -35,16 +35,17 @@
if os.name == 'nt':
from win32com.client import Dispatch
+ import win32com
import winreg
import win32ui
import pywintypes
from openlp.core.lib import ScreenList
+from openlp.core.common import Registry
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
from openlp.core.common import trace_error_handler
from .presentationcontroller import PresentationController, PresentationDocument
-
log = logging.getLogger(__name__)
@@ -134,6 +135,7 @@
self.controller.process.Presentations.Open(self.file_path, False, False, True)
self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
self.create_thumbnails()
+ self.create_titles_and_notes()
# Powerpoint 2013 pops up when loading a file, so we minimize it again
if self.presentation.Application.Version == u'15.0':
try:
@@ -390,6 +392,29 @@
"""
return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
+ def create_titles_and_notes(self):
+ """
+ Writes the list of titles (one per slide)
+ to 'titles.txt'
+ and the notes to 'slideNotes[x].txt'
+ in the thumbnails directory
+ """
+ titles = []
+ notes = []
+ for slide in self.presentation.Slides:
+ try:
+ text = slide.Shapes.Title.TextFrame.TextRange.Text
+ except Exception as e:
+ log.exception(e)
+ text = ''
+ titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n')
+ note = _get_text_from_shapes(slide.NotesPage.Shapes)
+ if len(note) == 0:
+ note = ' '
+ notes.append(note)
+ self.save_titles_and_notes(titles, notes)
+ return
+
def show_error_msg(self):
"""
Stop presentation and display an error message.
@@ -408,8 +433,8 @@
:param shapes: A set of shapes to search for text.
"""
text = ''
- for index in range(shapes.Count):
- shape = shapes(index + 1)
- if shape.HasTextFrame:
- text += shape.TextFrame.TextRange.Text + '\n'
+ for shape in shapes:
+ if shape.PlaceholderFormat.Type == 2: # 2 from is enum PpPlaceholderType.ppPlaceholderBody
+ if shape.HasTextFrame and shape.TextFrame.HasText:
+ text += shape.TextFrame.TextRange.Text + '\n'
return text
=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py 2014-03-29 19:56:20 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py 2014-07-15 10:08:10 +0000
@@ -29,6 +29,11 @@
import logging
import os
+import logging
+import zipfile
+import re
+from xml.etree import ElementTree
+
if os.name == 'nt':
from ctypes import cdll
@@ -125,14 +130,14 @@
temp_folder = self.get_temp_folder()
size = ScreenList().current['size']
rect = RECT(size.x(), size.y(), size.right(), size.bottom())
- file_path = os.path.normpath(self.file_path)
+ self.file_path = os.path.normpath(self.file_path)
preview_path = os.path.join(temp_folder, 'slide')
# Ensure that the paths are null terminated
- file_path = file_path.encode('utf-16-le') + b'\0'
+ self.file_path = self.file_path.encode('utf-16-le') + b'\0'
preview_path = preview_path.encode('utf-16-le') + b'\0'
if not os.path.isdir(temp_folder):
os.makedirs(temp_folder)
- self.ppt_id = self.controller.process.OpenPPT(file_path, None, rect, preview_path)
+ self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path)
if self.ppt_id >= 0:
self.create_thumbnails()
self.stop_presentation()
@@ -152,6 +157,69 @@
path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1))
self.convert_thumbnail(path, idx + 1)
+ def create_titles_and_notes(self):
+ """
+ Extracts the titles and notes from the zipped file
+ and writes the list of titles (one per slide)
+ to 'titles.txt'
+ and the notes to 'slideNotes[x].txt'
+ in the thumbnails directory
+ """
+ titles = None
+ notes = None
+ filename = os.path.normpath(self.file_path)
+ # let's make sure we have a valid zipped presentation
+ if os.path.exists(filename) and zipfile.is_zipfile(filename):
+ namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
+ "a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
+ # open the file
+ with zipfile.ZipFile(filename) as zip_file:
+ # find the presentation.xml to get the slide count
+ with zip_file.open('ppt/presentation.xml') as pres:
+ tree = ElementTree.parse(pres)
+ nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces)
+ # initialize the lists
+ titles = ['' for i in range(len(nodes))]
+ notes = ['' for i in range(len(nodes))]
+ # loop thru the file list to find slides and notes
+ for zip_info in zip_file.infolist():
+ node_type = ''
+ index = -1
+ list_to_add = None
+ # check if it is a slide
+ match = re.search("slides/slide(.+)\.xml", zip_info.filename)
+ if match:
+ index = int(match.group(1))-1
+ node_type = 'ctrTitle'
+ list_to_add = titles
+ # or a note
+ match = re.search("notesSlides/notesSlide(.+)\.xml", zip_info.filename)
+ if match:
+ index = int(match.group(1))-1
+ node_type = 'body'
+ list_to_add = notes
+ # if it is one of our files, index shouldn't be -1
+ if index >= 0:
+ with zip_file.open(zip_info) as zipped_file:
+ tree = ElementTree.parse(zipped_file)
+ text = ''
+ nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t",
+ namespaces=namespaces)
+ # if we found any content
+ if nodes and len(nodes) > 0:
+ for node in nodes:
+ if len(text) > 0:
+ text += '\n'
+ text += node.text
+ # Let's remove the \n from the titles and
+ # just add one at the end
+ if node_type == 'ctrTitle':
+ text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n'
+ list_to_add[index] = text
+ # now let's write the files
+ self.save_titles_and_notes(titles, notes)
+ return
+
def close_presentation(self):
"""
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
=== added file 'openlp/plugins/presentations/lib/pptviewlib/test.pptx'
Binary files openlp/plugins/presentations/lib/pptviewlib/test.pptx 1970-01-01 00:00:00 +0000 and openlp/plugins/presentations/lib/pptviewlib/test.pptx 2014-07-15 10:08:10 +0000 differ
=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py 2014-05-02 06:42:17 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py 2014-07-15 10:08:10 +0000
@@ -293,6 +293,49 @@
"""
return ''
+ def get_titles_and_notes(self):
+ """
+ Reads the titles from the titles file and
+ the notes files and returns the content in two lists
+ """
+ titles = []
+ notes = []
+ titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
+ if os.path.exists(titles_file):
+ try:
+ with open(titles_file) as fi:
+ titles = fi.read().splitlines()
+ except:
+ log.exception('Failed to open/read existing titles file')
+ titles = []
+ for slide_no, title in enumerate(titles, 1):
+ notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
+ note = ''
+ if os.path.exists(notes_file):
+ try:
+ with open(notes_file) as fn:
+ note = fn.read()
+ except:
+ log.exception('Failed to open/read notes file')
+ note = ''
+ notes.append(note)
+ return titles, notes
+
+ def save_titles_and_notes(self, titles, notes):
+ """
+ Performs the actual persisting of titles to the titles.txt
+ and notes to the slideNote%.txt
+ """
+ if titles:
+ titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
+ with open(titles_file, mode='w') as fo:
+ fo.writelines(titles)
+ if notes:
+ for slide_no, note in enumerate(notes, 1):
+ notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
+ with open(notes_file, mode='w') as fn:
+ fn.write(note)
+
class PresentationController(object):
"""
@@ -427,3 +470,12 @@
def close_presentation(self):
pass
+
+
+class TextType(object):
+ """
+ Type Enumeration for Types of Text to request
+ """
+ Title = 0
+ SlideText = 1
+ Notes = 2
=== modified file 'openlp/plugins/remotes/html/index.html'
--- openlp/plugins/remotes/html/index.html 2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/index.html 2014-07-15 10:08:10 +0000
@@ -120,6 +120,21 @@
<a href="#" id="controller-previous" data-role="button" data-icon="arrow-l">${prev}</a>
<a href="#" id="controller-next" data-role="button" data-icon="arrow-r" data-iconpos="right">${next}</a>
</div>
+ <div data-role="controlgroup" data-type="horizontal" style="float:left">
+ <a href="#settings" id="controller-settings" data-role="button" data-icon="gear" data-rel="dialog">${settings}</a>
+ </div>
+ </div>
+</div>
+<div data-role="page" id="settings">
+ <div data-role="header" data-position="inline" data-theme="b">
+ <h1>${settings}</h1>
+ </div>
+ <div data-role="content">
+ Display thumbnails:
+ <div data-role="controlgroup" data-type="horizontal">
+ <a href="#" id="display-thumbnails" data-role="button">Yes</a>
+ <a href="#" id="dont-display-thumbnails" data-role="button">No</a>
+ </div>
</div>
</div>
<div data-role="page" id="alerts">
=== modified file 'openlp/plugins/remotes/html/openlp.js'
--- openlp/plugins/remotes/html/openlp.js 2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/openlp.js 2014-07-15 10:08:10 +0000
@@ -87,16 +87,26 @@
var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
ul.html("");
for (idx in data.results.slides) {
- var text = data.results.slides[idx]["tag"];
+ var indexInt = parseInt(idx,10);
+ var slide = data.results.slides[idx];
+ var text = slide["tag"];
if (text != "") text = text + ": ";
- text = text + data.results.slides[idx]["text"];
+ if (slide["title"])
+ text += slide["title"]
+ else
+ text += slide["text"];
+ if (slide["notes"])
+ text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["notes"] + "</div>");
text = text.replace(/\n/g, '<br />');
+ if (slide["img"] && OpenLP.showThumbnails)
+ text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails80x80/") + "'>";
var li = $("<li data-icon=\"false\">").append(
- $("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(text));
- if (data.results.slides[idx]["selected"]) {
+ $("<a href=\"#\">").html(text));
+ if (slide["selected"]) {
li.attr("data-theme", "e");
}
li.children("a").click(OpenLP.setSlide);
+ li.find("*").attr("value", indexInt );
ul.append(li);
}
OpenLP.currentItem = data.results.item;
@@ -241,6 +251,17 @@
}
);
},
+ displayThumbnails: function (event) {
+ event.preventDefault();
+ var target = $(event.target);
+ OpenLP.showThumbnails = target.text() == "No" ? false : true;
+ var dt = new Date();
+ dt.setTime(dt.getTime() + 365 * 24 * 60 * 60 * 1000);
+ document.cookie = "displayThumbs=" + OpenLP.showThumbnails + "; expires=" +
+ dt.toGMTString() + "; path=/";
+ OpenLP.loadController();
+ $("#settings").dialog("close");
+ },
search: function (event) {
event.preventDefault();
var query = OpenLP.escapeString($("#search-text").val())
@@ -320,12 +341,24 @@
},
escapeString: function (string) {
return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")
- }
+ },
+ showThumbnails: false
}
// Initial jQueryMobile options
$(document).bind("mobileinit", function(){
$.mobile.defaultDialogTransition = "none";
$.mobile.defaultPageTransition = "none";
+ var cookies = document.cookie;
+ if( cookies )
+ {
+ var allcookies = cookies.split(";")
+ for(ii = 0; ii < allcookies.length; ii++)
+ {
+ var parts = allcookies[ii].split("=");
+ if(parts.length == 2 && parts[0] == "displayThumbs")
+ OpenLP.showThumbnails = (parts[1]=='true');
+ }
+ }
});
// Service Manager
$("#service-manager").live("pagebeforeshow", OpenLP.loadService);
@@ -345,6 +378,8 @@
$("#controller-theme").live("click", OpenLP.themeDisplay);
$("#controller-desktop").live("click", OpenLP.desktopDisplay);
$("#controller-show").live("click", OpenLP.showDisplay);
+$("#display-thumbnails").live("click", OpenLP.displayThumbnails);
+$("#dont-display-thumbnails").live("click", OpenLP.displayThumbnails);
// Alerts
$("#alert-submit").live("click", OpenLP.showAlert);
// Search
=== modified file 'openlp/plugins/remotes/html/stage.js'
--- openlp/plugins/remotes/html/stage.js 2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/html/stage.js 2014-07-15 10:08:10 +0000
@@ -102,7 +102,21 @@
$("#verseorder span").removeClass("currenttag");
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
- var text = slide["text"];
+ var text = "";
+ // use title if available
+ if (slide["title"]) {
+ text = slide["title"];
+ } else {
+ text = slide["text"];
+ }
+ // use thumbnail if available
+ if (slide["img"]) {
+ text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
+ }
+ // use notes if available
+ if (slide["notes"]) {
+ text += '<br />' + slide["notes"];
+ }
text = text.replace(/\n/g, "<br />");
$("#currentslide").html(text);
text = "";
@@ -110,7 +124,11 @@
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "<p class=\"nextslide\">";
- text = text + OpenLP.currentSlides[idx]["text"];
+ if (OpenLP.currentSlides[idx]["title"]) {
+ text = text + OpenLP.currentSlides[idx]["title"];
+ } else {
+ text = text + OpenLP.currentSlides[idx]["text"];
+ }
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "</p>";
else
=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py 2014-04-19 05:09:54 +0000
+++ openlp/plugins/remotes/lib/httprouter.py 2014-07-15 10:08:10 +0000
@@ -125,7 +125,7 @@
from PyQt4 import QtCore
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate
-from openlp.core.lib import PluginStatus, StringContent, image_to_byte
+from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities
log = logging.getLogger(__name__)
FILE_TYPES = {
@@ -159,6 +159,7 @@
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
('^/(main)$', {'function': self.serve_file, 'secure': False}),
(r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
+ (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
(r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
(r'^/main/image$', {'function': self.main_image, 'secure': False}),
@@ -328,7 +329,8 @@
'no_results': translate('RemotePlugin.Mobile', 'No Results'),
'options': translate('RemotePlugin.Mobile', 'Options'),
'service': translate('RemotePlugin.Mobile', 'Service'),
- 'slides': translate('RemotePlugin.Mobile', 'Slides')
+ 'slides': translate('RemotePlugin.Mobile', 'Slides'),
+ 'settings': translate('RemotePlugin.Mobile', 'Settings'),
}
def serve_file(self, file_name=None):
@@ -380,6 +382,35 @@
content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type
+ def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
+ """
+ Serve an image file. If not found return 404.
+ """
+ log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
+ supported_controllers = ['presentations', 'images']
+ if not dimensions:
+ dimensions = ''
+ content = ''
+ content_type = None
+ if controller_name and file_name:
+ if controller_name in supported_controllers:
+ full_path = urllib.parse.unquote(file_name)
+ if '..' not in full_path: # no hacking please
+ full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
+ 'thumbnails/' + full_path))
+ if os.path.exists(full_path):
+ path, just_file_name = os.path.split(full_path)
+ self.image_manager.add_image(full_path, just_file_name, None, dimensions)
+ ext, content_type = self.get_content_type(full_path)
+ image = self.image_manager.get_image(full_path, just_file_name, dimensions)
+ content = image_to_byte(image, False)
+ if len(content) == 0:
+ return self.do_not_found()
+ self.send_response(200)
+ self.send_header('Content-type', content_type)
+ self.end_headers()
+ return content
+
def poll(self):
"""
Poll OpenLP to determine the current slide number and item name.
@@ -465,11 +496,28 @@
item['tag'] = str(index + 1)
item['text'] = str(frame['text'])
item['html'] = str(frame['html'])
+ elif current_item.is_image():
+ item['tag'] = str(index + 1)
+ thumbnail_path = os.path.sep + os.path.join('images', 'thumbnails', frame['title'])
+ item['img'] = urllib.request.pathname2url(thumbnail_path)
+ item['text'] = str(frame['title'])
+ item['html'] = str(frame['title'])
else:
item['tag'] = str(index + 1)
+ if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
+ item['title'] = str(frame['display_title'])
+ if current_item.is_capable(ItemCapabilities.HasNotes):
+ item['notes'] = str(frame['notes'])
+ if current_item.is_capable(ItemCapabilities.HasThumbnails):
+ # If the file is under our app directory tree send the portion after the match
+ data_path = AppLocation.get_data_path()
+ if frame['image'][0:len(data_path)] == data_path:
+ item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
item['selected'] = (self.live_controller.selected_row == index)
+ if current_item.notes:
+ item['notes'] = item.get('notes', '') + '\n' + current_item.notes
data.append(item)
json_data = {'results': {'slides': data}}
if current_item:
=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py 2014-04-02 18:51:21 +0000
+++ tests/functional/openlp_core_common/test_applocation.py 2014-07-15 10:08:10 +0000
@@ -162,9 +162,9 @@
patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \
patch('openlp.core.common.applocation.os.path.split') as mocked_split, \
patch('openlp.core.common.applocation.sys') as mocked_sys:
- mocked_abspath.return_value = 'plugins/dir'
+ mocked_abspath.return_value = os.path.join('plugins', 'dir')
mocked_split.return_value = ['openlp']
- mocked_get_frozen_path.return_value = 'plugins/dir'
+ mocked_get_frozen_path.return_value = os.path.join('plugins', 'dir')
mocked_sys.frozen = 1
mocked_sys.argv = ['openlp']
@@ -172,7 +172,7 @@
directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN: The correct directory should be returned
- self.assertEqual('plugins/dir', directory, 'Directory should be "plugins/dir"')
+ self.assertEqual(os.path.join('plugins', 'dir'), directory, 'Directory should be "plugins/dir"')
def get_frozen_path_in_unfrozen_app_test(self):
"""
=== modified file 'tests/functional/openlp_core_lib/test_image_manager.py'
--- tests/functional/openlp_core_lib/test_image_manager.py 2014-06-04 04:54:44 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py 2014-07-15 10:08:10 +0000
@@ -69,16 +69,17 @@
Test the Image Manager setup basic functionality
"""
# GIVEN: the an image add to the image manager
- self.image_manager.add_image(TEST_PATH, 'church.jpg', None)
+ full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+ self.image_manager.add_image(full_path, 'church.jpg', None)
# WHEN the image is retrieved
- image = self.image_manager.get_image(TEST_PATH, 'church.jpg')
+ image = self.image_manager.get_image(full_path, 'church.jpg')
# THEN returned record is a type of image
self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
# WHEN: The image bytes are requested.
- byte_array = self.image_manager.get_image_bytes(TEST_PATH, 'church.jpg')
+ byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg')
# THEN: Type should be a str.
self.assertEqual(isinstance(byte_array, str), True, 'The returned object should be a str')
@@ -89,6 +90,38 @@
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
+ def different_dimension_image_test(self):
+ """
+ Test the Image Manager with dimensions
+ """
+ # GIVEN: add an image with specific dimensions
+ full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+ self.image_manager.add_image(full_path, 'church.jpg', None, '80x80')
+
+ # WHEN: the image is retrieved
+ image = self.image_manager.get_image(full_path, 'church.jpg', '80x80')
+
+ # THEN: The return should be of type image
+ self.assertEqual(isinstance(image, QtGui.QImage), True, 'The returned object should be a QImage')
+
+ # WHEN: adding the same image with different dimensions
+ self.image_manager.add_image(full_path, 'church.jpg', None, '100x100')
+
+ # THEN: the cache should contain two pictures
+ self.assertEqual(len(self.image_manager._cache), 2,
+ 'Image manager should consider two dimensions of the same picture as different')
+
+ # WHEN: adding the same image with first dimensions
+ self.image_manager.add_image(full_path, 'church.jpg', None, '80x80')
+
+ # THEN: the cache should still contain only two pictures
+ self.assertEqual(len(self.image_manager._cache), 2, 'Same dimensions should not be added again')
+
+ # WHEN: calling with correct image, but wrong dimensions
+ with self.assertRaises(KeyError) as context:
+ self.image_manager.get_image(full_path, 'church.jpg', '120x120')
+ self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing dimension')
+
def process_cache_test(self):
"""
Test the process_cache method
@@ -151,7 +184,7 @@
:param image: The name of the image. E. g. ``image1``
"""
- return self.image_manager._cache[(TEST_PATH, image)].priority
+ return self.image_manager._cache[(TEST_PATH, image, '')].priority
def mocked_resize_image(self, *args):
"""
=== modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py 2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py 2014-07-15 10:08:10 +0000
@@ -32,13 +32,11 @@
import os
from unittest import TestCase
-
from tests.functional import MagicMock, patch
from tests.utils import assert_length, convert_file_service_item
from openlp.core.common import Registry
-from openlp.core.lib import ItemCapabilities, ServiceItem
-
+from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
VERSE = 'The Lord said to {r}Noah{/r}: \n'\
'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n'\
@@ -126,7 +124,7 @@
# THEN: We should get back a valid service item
self.assertTrue(service_item.is_valid, 'The new service item should be valid')
- self.assertEqual(test_file, service_item.get_rendered_frame(0),
+ self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)),
'The first frame should match the path to the image')
self.assertEqual(frame_array, service_item.get_frames()[0],
'The return should match frame array1')
@@ -153,8 +151,8 @@
# GIVEN: A new service item and a mocked add icon function
image_name1 = 'image_1.jpg'
image_name2 = 'image_2.jpg'
- test_file1 = os.path.join('/home/openlp', image_name1)
- test_file2 = os.path.join('/home/openlp', image_name2)
+ test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
+ test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
frame_array1 = {'path': test_file1, 'title': image_name1}
frame_array2 = {'path': test_file2, 'title': image_name2}
@@ -206,3 +204,41 @@
'This service item should be able to be run in a can be made to Loop')
self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend),
'This service item should be able to have new items added to it')
+
+ def add_from_command_for_a_presentation_test(self):
+ """
+ Test the Service Item - adding a presentation
+ """
+ # GIVEN: A service item, a mocked icon and presentation data
+ service_item = ServiceItem(None)
+ presentation_name = 'test.pptx'
+ image = MagicMock()
+ display_title = 'DisplayTitle'
+ notes = 'Note1\nNote2\n'
+ frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH,
+ 'display_title': display_title, 'notes': notes}
+
+ # WHEN: adding presentation to service_item
+ service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes)
+
+ # THEN: verify that it is setup as a Command and that the frame data matches
+ self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
+ self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
+
+ def add_from_comamnd_without_display_title_and_notes_test(self):
+ """
+ Test the Service Item - add from command, but not presentation
+ """
+ # GIVEN: A new service item, a mocked icon and image data
+ service_item = ServiceItem(None)
+ image_name = 'test.img'
+ image = MagicMock()
+ frame = {'title': image_name, 'image': image, 'path': TEST_PATH,
+ 'display_title': None, 'notes': None}
+
+ # WHEN: adding image to service_item
+ service_item.add_from_command(TEST_PATH, image_name, image)
+
+ # THEN: verify that it is setup as a Command and that the frame data matches
+ self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
+ self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
=== added file 'tests/functional/openlp_plugins/presentations/test_impresscontroller.py'
--- tests/functional/openlp_plugins/presentations/test_impresscontroller.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_impresscontroller.py 2014-07-15 10:08:10 +0000
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; version 2 of the License. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
+###############################################################################
+"""
+Functional tests to test the Impress class and related methods.
+"""
+from unittest import TestCase
+import os
+import shutil
+from tempfile import mkdtemp
+
+from tests.functional import patch, MagicMock
+from tests.utils.constants import TEST_RESOURCES_PATH
+from tests.helpers.testmixin import TestMixin
+
+from openlp.plugins.presentations.lib.impresscontroller import \
+ ImpressController, ImpressDocument, TextType
+
+
+class TestImpressController(TestCase, TestMixin):
+ """
+ Test the ImpressController Class
+ """
+
+ def setUp(self):
+ """
+ Set up the patches and mocks need for all tests.
+ """
+ self.get_application()
+ self.build_settings()
+ self.mock_plugin = MagicMock()
+ self.temp_folder = mkdtemp()
+ self.mock_plugin.settings_section = self.temp_folder
+
+ def tearDown(self):
+ """
+ Stop the patches
+ """
+ self.destroy_settings()
+ shutil.rmtree(self.temp_folder)
+
+ def constructor_test(self):
+ """
+ Test the Constructor from the ImpressController
+ """
+ # GIVEN: No presentation controller
+ controller = None
+
+ # WHEN: The presentation controller object is created
+ controller = ImpressController(plugin=self.mock_plugin)
+
+ # THEN: The name of the presentation controller should be correct
+ self.assertEqual('Impress', controller.name,
+ 'The name of the presentation controller should be correct')
+
+
+class TestImpressDocumnt(TestCase):
+ """
+ Test the ImpressDocument Class
+ """
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+ self.ppc = ImpressController(mocked_plugin)
+ self.doc = ImpressDocument(self.ppc, self.file_name)
+
+ def create_titles_and_notes_test(self):
+ """
+ Test ImpressDocument.create_titles_and_notes
+ """
+ # GIVEN: mocked PresentationController.save_titles_and_notes with
+ # 0 pages and the LibreOffice Document
+ self.doc.save_titles_and_notes = MagicMock()
+ self.doc.document = MagicMock()
+ self.doc.document.getDrawPages.return_value = MagicMock()
+ self.doc.document.getDrawPages().getCount.return_value = 0
+
+ # WHEN reading the titles and notes
+ self.doc.create_titles_and_notes()
+
+ # THEN save_titles_and_notes should have been called with empty arrays
+ self.doc.save_titles_and_notes.assert_called_once_with([], [])
+
+ # GIVEN: reset mock and set it to 2 pages
+ self.doc.save_titles_and_notes.reset_mock()
+ self.doc.document.getDrawPages().getCount.return_value = 2
+
+ # WHEN: a new call to create_titles_and_notes
+ self.doc.create_titles_and_notes()
+
+ # THEN: save_titles_and_notes should have been called once with
+ # two arrays of two elements
+ self.doc.save_titles_and_notes.assert_called_once_with(['\n', '\n'], [' ', ' '])
+
+ def get_text_from_page_out_of_bound_test(self):
+ """
+ Test ImpressDocument.__get_text_from_page with out-of-bounds index
+ """
+ # GIVEN: mocked LibreOffice Document with one slide,
+ # two notes and three texts
+ self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+ # WHEN: __get_text_from_page is called with an index of 0x00
+ result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Notes)
+
+ # THEN: the result should be an empty string
+ self.assertEqual(result, '', 'Result should be an empty string')
+
+ # WHEN: regardless of the type of text, index 0x00 is out of bounds
+ result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Title)
+
+ # THEN: result should be an empty string
+ self.assertEqual(result, '', 'Result should be an empty string')
+
+ # WHEN: when called with 2, it should also be out of bounds
+ result = self.doc._ImpressDocument__get_text_from_page(2, TextType.SlideText)
+
+ # THEN: result should be an empty string ... and, getByIndex should
+ # have never been called
+ self.assertEqual(result, '', 'Result should be an empty string')
+ self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
+ 'There should be no call to getByIndex')
+
+ def get_text_from_page_wrong_type_test(self):
+ """
+ Test ImpressDocument.__get_text_from_page with wrong TextType
+ """
+ # GIVEN: mocked LibreOffice Document with one slide, two notes and
+ # three texts
+ self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+ # WHEN: called with TextType 3
+ result = self.doc._ImpressDocument__get_text_from_page(1, 3)
+
+ # THEN: result should be an empty string
+ self.assertEqual(result, '', 'Result should be and empty string')
+ self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
+ 'There should be no call to getByIndex')
+
+ def get_text_from_page_valid_params_test(self):
+ """
+ Test ImpressDocument.__get_text_from_page with valid parameters
+ """
+ # GIVEN: mocked LibreOffice Document with one slide,
+ # two notes and three texts
+ self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
+
+ # WHEN: __get_text_from_page is called to get the Notes
+ result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Notes)
+
+ # THEN: result should be 'Note\nNote\n'
+ self.assertEqual(result, 'Note\nNote\n', 'Result should be \'Note\\n\' times the count of notes in the page')
+
+ # WHEN: get the Title
+ result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Title)
+
+ # THEN: result should be 'Title\n'
+ self.assertEqual(result, 'Title\n', 'Result should be exactly \'Title\\n\'')
+
+ # WHEN: get all text
+ result = self.doc._ImpressDocument__get_text_from_page(1, TextType.SlideText)
+
+ # THEN: result should be 'Title\nString\nString\n'
+ self.assertEqual(result, 'Title\nString\nString\n', 'Result should be exactly \'Title\\nString\\nString\\n\'')
+
+ def _mock_a_LibreOffice_document(self, page_count, note_count, text_count):
+ """
+ Helper function, creates a mock libreoffice document.
+
+ :param page_count: Number of pages in the document
+ :param note_count: Number of note pages in the document
+ :param text_count: Number of text pages in the document
+ """
+ pages = MagicMock()
+ page = MagicMock()
+ pages.getByIndex.return_value = page
+ notes_page = MagicMock()
+ notes_page.getCount.return_value = note_count
+ shape = MagicMock()
+ shape.supportsService.return_value = True
+ shape.getString.return_value = 'Note'
+ notes_page.getByIndex.return_value = shape
+ page.getNotesPage.return_value = notes_page
+ page.getCount.return_value = text_count
+ page.getByIndex.side_effect = self._get_page_shape_side_effect
+ pages.getCount.return_value = page_count
+ document = MagicMock()
+ document.getDrawPages.return_value = pages
+ document.getByIndex.return_value = page
+ return document
+
+ def _get_page_shape_side_effect(*args):
+ """
+ Helper function.
+ """
+ page_shape = MagicMock()
+ page_shape.supportsService.return_value = True
+ if args[1] == 0:
+ page_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
+ page_shape.getString.return_value = 'Title'
+ else:
+ page_shape.getString.return_value = 'String'
+ return page_shape
=== modified file 'tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py 2014-07-03 11:21:12 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py 2014-07-15 10:08:10 +0000
@@ -38,8 +38,10 @@
from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin
+from tests.utils.constants import TEST_RESOURCES_PATH
-from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument
+from openlp.plugins.presentations.lib.powerpointcontroller import PowerpointController, PowerpointDocument,\
+ _get_text_from_shapes
class TestPowerpointController(TestCase, TestMixin):
@@ -79,7 +81,7 @@
'The name of the presentation controller should be correct')
-class TestPowerpointDocument(TestCase):
+class TestPowerpointDocument(TestCase, TestMixin):
"""
Test the PowerpointDocument Class
"""
@@ -88,6 +90,11 @@
"""
Set up the patches and mocks need for all tests.
"""
+ self.get_application()
+ self.build_settings()
+ self.mock_plugin = MagicMock()
+ self.temp_folder = mkdtemp()
+ self.mock_plugin.settings_section = self.temp_folder
self.powerpoint_document_stop_presentation_patcher = patch(
'openlp.plugins.presentations.lib.powerpointcontroller.PowerpointDocument.stop_presentation')
self.presentation_document_get_temp_folder_patcher = patch(
@@ -100,6 +107,8 @@
self.mock_controller = MagicMock()
self.mock_presentation = MagicMock()
self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
+ self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+ self.real_controller = PowerpointController(self.mock_plugin)
def tearDown(self):
"""
@@ -108,6 +117,8 @@
self.powerpoint_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop()
+ self.destroy_settings()
+ shutil.rmtree(self.temp_folder)
def show_error_msg_test(self):
"""
@@ -129,3 +140,95 @@
'integration and the presentation will be stopped.'
' Restart the presentation if you wish to '
'present it.')
+
+ # add _test to the following if necessary
+ def verify_loading_document(self):
+ """
+ Test loading a document in PowerPoint
+ """
+ if os.name == 'nt' and self.real_controller.check_available():
+ # GIVEN: A PowerpointDocument and a presentation
+ doc = PowerpointDocument(self.real_controller, self.file_name)
+
+ # WHEN: loading the filename
+ doc.load_presentation()
+ result = doc.is_loaded()
+
+ # THEN: result should be true
+ self.assertEqual(result, True, 'The result should be True')
+ else:
+ self.skipTest('Powerpoint not available, skipping test.')
+
+ def create_titles_and_notes_test(self):
+ """
+ Test creating the titles from PowerPoint
+ """
+ if os.name == 'nt' and self.real_controller.check_available():
+ # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+ self.doc = PowerpointDocument(self.real_controller, self.file_name)
+ self.doc.save_titles_and_notes = MagicMock()
+ self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+ slide = MagicMock()
+ slide.Shapes.Title.TextFrame.TextRange.Text = 'SlideText'
+ pres = MagicMock()
+ pres.Slides = [slide, slide]
+ self.doc.presentation = pres
+
+ # WHEN reading the titles and notes
+ self.doc.create_titles_and_notes()
+
+ # THEN the save should have been called exactly once with 2 titles and 2 notes
+ self.doc.save_titles_and_notes.assert_called_once_with(['SlideText\n', 'SlideText\n'], [' ', ' '])
+ else:
+ self.skipTest('Powerpoint not available, skipping test.')
+
+ def create_titles_and_notes_with_no_slides_test(self):
+ """
+ Test creating the titles from PowerPoint when it returns no slides
+ """
+ if os.name == 'nt' and self.real_controller.check_available():
+ # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+ doc = PowerpointDocument(self.real_controller, self.file_name)
+ doc.save_titles_and_notes = MagicMock()
+ doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+ pres = MagicMock()
+ pres.Slides = []
+ doc.presentation = pres
+
+ # WHEN reading the titles and notes
+ doc.create_titles_and_notes()
+
+ # THEN the save should have been called exactly once with empty titles and notes
+ doc.save_titles_and_notes.assert_called_once_with([], [])
+ else:
+ self.skipTest('Powerpoint not available, skipping test.')
+
+ def get_text_from_shapes_test(self):
+ """
+ Test getting text from powerpoint shapes
+ """
+ # GIVEN: mocked shapes
+ shape = MagicMock()
+ shape.PlaceholderFormat.Type = 2
+ shape.HasTextFrame = shape.TextFrame.HasText = True
+ shape.TextFrame.TextRange.Text = 'slideText'
+ shapes = [shape, shape]
+
+ # WHEN: getting the text
+ result = _get_text_from_shapes(shapes)
+
+ # THEN: it should return the text
+ self.assertEqual(result, 'slideText\nslideText\n', 'result should match \'slideText\nslideText\n\'')
+
+ def get_text_from_shapes_with_no_shapes_test(self):
+ """
+ Test getting text from powerpoint shapes with no shapes
+ """
+ # GIVEN: empty shapes array
+ shapes = []
+
+ # WHEN: getting the text
+ result = _get_text_from_shapes(shapes)
+
+ # THEN: it should not fail but return empty string
+ self.assertEqual(result, '', 'result should be empty')
=== modified file 'tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py 2014-04-20 20:19:21 +0000
+++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py 2014-07-15 10:08:10 +0000
@@ -4,8 +4,8 @@
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2013 Raoul Snyman #
-# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
@@ -39,6 +39,7 @@
from tests.functional import MagicMock, patch
from tests.helpers.testmixin import TestMixin
+from tests.utils.constants import TEST_RESOURCES_PATH
from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
@@ -130,7 +131,7 @@
"""
Set up the patches and mocks need for all tests.
"""
- self.os_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os')
+ self.os_isdir_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.isdir')
self.pptview_document_create_thumbnails_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
self.pptview_document_stop_presentation_patcher = patch(
@@ -141,41 +142,40 @@
'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
self.rect_patcher = MagicMock()
-
- self.mock_os = self.os_patcher.start()
+ self.mock_os_isdir = self.os_isdir_patcher.start()
self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start()
self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start()
self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
self.mock_rect = self.rect_patcher.start()
self.mock_screen_list = self.screen_list_patcher.start()
-
self.mock_controller = MagicMock()
self.mock_presentation = MagicMock()
-
- self.mock_presentation_document_get_temp_folder.return_value = 'temp folder'
+ self.temp_folder = mkdtemp()
+ self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
def tearDown(self):
"""
Stop the patches
"""
- self.os_patcher.stop()
+ self.os_isdir_patcher.stop()
self.pptview_document_create_thumbnails_patcher.stop()
self.pptview_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop()
self.rect_patcher.stop()
self.screen_list_patcher.stop()
+ shutil.rmtree(self.temp_folder)
def load_presentation_succesfull_test(self):
"""
Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
"""
# GIVEN: A reset mocked_os
- self.mock_os.reset()
+ self.mock_os_isdir.reset()
# WHEN: The temporary directory exists and OpenPPT returns successfully (not -1)
- self.mock_os.path.isdir.return_value = True
+ self.mock_os_isdir.return_value = True
self.mock_controller.process.OpenPPT.return_value = 0
instance = PptviewDocument(self.mock_controller, self.mock_presentation)
instance.file_path = 'test\path.ppt'
@@ -191,17 +191,78 @@
Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
not successfully opened
"""
- # GIVEN: A reset mocked_os
- self.mock_os.reset()
+ # GIVEN: A reset mock_os_isdir
+ self.mock_os_isdir.reset()
# WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
- self.mock_os.path.isdir.return_value = False
- self.mock_controller.process.OpenPPT.return_value = -1
- instance = PptviewDocument(self.mock_controller, self.mock_presentation)
- instance.file_path = 'test\path.ppt'
- if os.name == 'nt':
- result = instance.load_presentation()
-
- # THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False
- self.mock_os.makedirs.assert_called_once_with('temp folder')
- self.assertFalse(result)
+ with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs:
+ self.mock_os_isdir.return_value = False
+ self.mock_controller.process.OpenPPT.return_value = -1
+ instance = PptviewDocument(self.mock_controller, self.mock_presentation)
+ instance.file_path = 'test\path.ppt'
+ if os.name == 'nt':
+ result = instance.load_presentation()
+
+ # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
+ mock_makedirs.assert_called_once_with(self.temp_folder)
+ self.assertFalse(result)
+
+ def create_titles_and_notes_test(self):
+ """
+ Test PowerpointController.create_titles_and_notes
+ """
+ # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
+ doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+ doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+ doc.save_titles_and_notes = MagicMock()
+
+ # WHEN reading the titles and notes
+ doc.create_titles_and_notes()
+
+ # THEN save_titles_and_notes should have been called once with empty arrays
+ doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'],
+ ['Notes for slide 1', 'Inserted', 'Notes for slide 2',
+ 'Notes \nfor slide 4', 'Notes for slide 3'])
+
+ def create_titles_and_notes_nonexistent_file_test(self):
+ """
+ Test PowerpointController.create_titles_and_notes with nonexistent file
+ """
+ # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
+ with patch('builtins.open') as mocked_open, \
+ patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
+ patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
+ mocked_dir_exists:
+ mocked_exists.return_value = False
+ mocked_dir_exists.return_value = False
+ doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+ doc.file_path = 'Idontexist.pptx'
+ doc.save_titles_and_notes = MagicMock()
+
+ # WHEN: Reading the titles and notes
+ doc.create_titles_and_notes()
+
+ # THEN: File existens should have been checked, and not have been opened.
+ doc.save_titles_and_notes.assert_called_once_with(None, None)
+ mocked_exists.assert_any_call('Idontexist.pptx')
+ self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.')
+
+ def create_titles_and_notes_invalid_file_test(self):
+ """
+ Test PowerpointController.create_titles_and_notes with invalid file
+ """
+ # GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file
+ with patch('builtins.open') as mocked_open, \
+ patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
+ mocked_is_zf.return_value = False
+ mocked_open.filesize = 10
+ doc = PptviewDocument(self.mock_controller, self.mock_presentation)
+ doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
+ doc.save_titles_and_notes = MagicMock()
+
+ # WHEN: reading the titles and notes
+ doc.create_titles_and_notes()
+
+ # THEN:
+ doc.save_titles_and_notes.assert_called_once_with(None, None)
+ self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once')
=== modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py 2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py 2014-07-15 10:08:10 +0000
@@ -1,158 +1,166 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2014 Raoul Snyman #
-# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
-# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
-# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
-# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
-# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
-# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
-# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
-# --------------------------------------------------------------------------- #
-# This program is free software; you can redistribute it and/or modify it #
-# under the terms of the GNU General Public License as published by the Free #
-# Software Foundation; version 2 of the License. #
-# #
-# This program is distributed in the hope that it will be useful, but WITHOUT #
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
-# more details. #
-# #
-# You should have received a copy of the GNU General Public License along #
-# with this program; if not, write to the Free Software Foundation, Inc., 59 #
-# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
-###############################################################################
-"""
-This module contains tests for the Presentation Controller.
-"""
-from unittest import TestCase
-
-from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
-from tests.functional import MagicMock, patch
-
-
-class TestPresentationController(TestCase):
- """
- Test the PresentationController.
- """
- # TODO: Items left to test
- # PresentationController
- # __init__
- # enabled
- # is_available
- # check_available
- # start_process
- # kill
- # add_document
- # remove_doc
- # close_presentation
- # _get_plugin_manager
-
- def constructor_test(self):
- """
- Test the Constructor
- """
- # GIVEN: No presentation controller
- controller = None
-
- # WHEN: The presentation controller object is created
- mock_plugin = MagicMock()
- mock_plugin.settings_section = ''
- controller = PresentationController(plugin=mock_plugin)
-
- # THEN: The name of the presentation controller should be correct
- self.assertEqual('PresentationController', controller.name,
- 'The name of the presentation controller should be correct')
-
-
-class TestPresentationDocument(TestCase):
- """
- Test the PresentationDocument Class
- """
- # TODO: Items left to test
- # PresentationDocument
- # __init__
- # load_presentation
- # presentation_deleted
- # get_file_name
- # get_thumbnail_folder
- # get_temp_folder
- # check_thumbnails
- # close_presentation
- # is_active
- # is_loaded
- # blank_screen
- # unblank_screen
- # is_blank
- # stop_presentation
- # start_presentation
- # get_slide_number
- # get_slide_count
- # goto_slide
- # next_step
- # previous_step
- # convert_thumbnail
- # get_thumbnail_path
- # poll_slidenumber
- # get_slide_text
- # get_slide_notes
-
- def setUp(self):
- """
- Set up the patches and mocks need for all tests.
- """
- self.check_directory_exists_patcher = \
- patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists')
- self.get_thumbnail_folder_patcher = \
- patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
- self._setup_patcher = \
- patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
-
- self.mock_check_directory_exists = self.check_directory_exists_patcher.start()
- self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
- self.mock_setup = self._setup_patcher.start()
-
- self.mock_controller = MagicMock()
-
- self.mock_get_thumbnail_folder.return_value = 'returned/path/'
-
- def tearDown(self):
- """
- Stop the patches
- """
- self.check_directory_exists_patcher.stop()
- self.get_thumbnail_folder_patcher.stop()
- self._setup_patcher.stop()
-
- def initialise_presentation_document_test(self):
- """
- Test the PresentationDocument __init__ method when initialising the PresentationDocument Class
- """
- # GIVEN: A reset mock_setup and mocked controller
- self.mock_setup.reset()
-
- # WHEN: Creating an instance of PresentationDocument
- PresentationDocument(self.mock_controller, 'Name')
-
- # THEN: PresentationDocument.__init__ should have been called with the correct arguments
- self.mock_setup.assert_called_once_with('Name')
-
- def presentation_document_setup_test(self):
- """
- Test the PresentationDocument _setup method when initialising the PresentationDocument Class
- """
- self._setup_patcher.stop()
-
- # GIVEN: A mocked controller, patched check_directory_exists_patcher and patched get_thumbnail_folder method
-
- # WHEN: Creating an instance of PresentationDocument
- PresentationDocument(self.mock_controller, 'Name')
-
- # THEN: check_directory_exists should have been called with the correct arguments
- self.mock_check_directory_exists.assert_called_once_with('returned/path/')
-
- self._setup_patcher.start()
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2014 Raoul Snyman #
+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; version 2 of the License. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
+###############################################################################
+"""
+Functional tests to test the PresentationController and PresentationDocument
+classes and related methods.
+"""
+from unittest import TestCase
+import os
+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
+from tests.functional import MagicMock, patch, mock_open
+
+FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder'
+
+
+class TestPresentationController(TestCase):
+ """
+ Test the PresentationController.
+ """
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ self.presentation = PresentationController(mocked_plugin)
+ self.document = PresentationDocument(self.presentation, '')
+
+ def constructor_test(self):
+ """
+ Test the Constructor
+ """
+ # GIVEN: A mocked plugin
+
+ # WHEN: The PresentationController is created
+
+ # THEN: The name of the presentation controller should be correct
+ self.assertEqual('PresentationController', self.presentation.name,
+ 'The name of the presentation controller should be correct')
+
+ def save_titles_and_notes_test(self):
+ """
+ Test PresentationDocument.save_titles_and_notes method with two valid lists
+ """
+ # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
+ mocked_open = mock_open()
+ with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+ titles = ['uno', 'dos']
+ notes = ['one', 'two']
+
+ # WHEN: calling save_titles_and_notes
+ mocked_get_thumbnail_folder.return_value = 'test'
+ self.document.save_titles_and_notes(titles, notes)
+
+ # THEN: the last call to open should have been for slideNotes2.txt
+ mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='w')
+ mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='w')
+ mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='w')
+ self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
+ mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
+ mocked_open().write.assert_called_any('one')
+ mocked_open().write.assert_called_any('two')
+
+ def save_titles_and_notes_with_None_test(self):
+ """
+ Test PresentationDocument.save_titles_and_notes method with no data
+ """
+ # GIVEN: None and an empty list and a mocked open and get_thumbnail_folder
+ with patch('builtins.open') as mocked_open, patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+ titles = None
+ notes = None
+
+ # WHEN: calling save_titles_and_notes
+ mocked_get_thumbnail_folder.return_value = 'test'
+ self.document.save_titles_and_notes(titles, notes)
+
+ # THEN: No file should have been created
+ self.assertEqual(mocked_open.call_count, 0, 'No file should be created')
+
+ def get_titles_and_notes_test(self):
+ """
+ Test PresentationDocument.get_titles_and_notes method
+ """
+ # GIVEN: A mocked open, get_thumbnail_folder and exists
+
+ with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \
+ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+ patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+ mocked_get_thumbnail_folder.return_value = 'test'
+ mocked_exists.return_value = True
+
+ # WHEN: calling get_titles_and_notes
+ result_titles, result_notes = self.document.get_titles_and_notes()
+
+ # THEN: it should return two items for the titles and two empty strings for the notes
+ self.assertIs(type(result_titles), list, 'result_titles should be of type list')
+ self.assertEqual(len(result_titles), 2, 'There should be two items in the titles')
+ self.assertIs(type(result_notes), list, 'result_notes should be of type list')
+ self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
+ self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened')
+ mocked_open.assert_any_call(os.path.join('test', 'titles.txt'))
+ mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'))
+ mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'))
+ self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
+
+ def get_titles_and_notes_with_file_not_found_test(self):
+ """
+ Test PresentationDocument.get_titles_and_notes method with file not found
+ """
+ # GIVEN: A mocked open, get_thumbnail_folder and exists
+ with patch('builtins.open') as mocked_open, \
+ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+ patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+ mocked_get_thumbnail_folder.return_value = 'test'
+ mocked_exists.return_value = False
+
+ # WHEN: calling get_titles_and_notes
+ result_titles, result_notes = self.document.get_titles_and_notes()
+
+ # THEN: it should return two empty lists
+ self.assertIs(type(result_titles), list, 'result_titles should be of type list')
+ self.assertEqual(len(result_titles), 0, 'there be no titles')
+ self.assertIs(type(result_notes), list, 'result_notes should be a list')
+ self.assertEqual(len(result_notes), 0, 'but the list should be empty')
+ self.assertEqual(mocked_open.call_count, 0, 'No calls to open files')
+ self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
+
+ def get_titles_and_notes_with_file_error_test(self):
+ """
+ Test PresentationDocument.get_titles_and_notes method with file errors
+ """
+ # GIVEN: A mocked open, get_thumbnail_folder and exists
+ with patch('builtins.open') as mocked_open, \
+ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
+ patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
+ mocked_get_thumbnail_folder.return_value = 'test'
+ mocked_exists.return_value = True
+ mocked_open.side_effect = IOError()
+
+ # WHEN: calling get_titles_and_notes
+ result_titles, result_notes = self.document.get_titles_and_notes()
+
+ # THEN: it should return two empty lists
+ self.assertIs(type(result_titles), list, 'result_titles should be a list')
=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py 2014-05-07 20:38:34 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py 2014-07-15 10:08:10 +0000
@@ -30,10 +30,12 @@
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
+import urllib.request
from unittest import TestCase
from openlp.core.common import Settings, Registry
from openlp.plugins.remotes.lib.httpserver import HttpRouter
+from urllib.parse import urlparse
from tests.functional import MagicMock, patch, mock_open
from tests.helpers.testmixin import TestMixin
@@ -186,3 +188,86 @@
self.router.send_response.assert_called_once_with(200)
self.router.send_header.assert_called_once_with('Content-type', 'text/html')
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+
+ def serve_thumbnail_without_params_test(self):
+ """
+ Test the serve_thumbnail routine without params
+ """
+ self.router.send_response = MagicMock()
+ self.router.send_header = MagicMock()
+ self.router.end_headers = MagicMock()
+ self.router.wfile = MagicMock()
+ self.router.serve_thumbnail()
+ self.router.send_response.assert_called_once_with(404)
+ self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
+ self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+
+ def serve_thumbnail_with_invalid_params_test(self):
+ """
+ Test the serve_thumbnail routine with invalid params
+ """
+ # GIVEN: Mocked send_header, send_response, end_headers and wfile
+ self.router.send_response = MagicMock()
+ self.router.send_header = MagicMock()
+ self.router.end_headers = MagicMock()
+ self.router.wfile = MagicMock()
+
+ # WHEN: pass a bad controller
+ self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png')
+
+ # THEN: a 404 should be returned
+ self.assertEqual(len(self.router.send_header.mock_calls), 1, 'One header')
+ self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response')
+ self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket')
+ self.router.send_response.assert_called_once_with(404)
+
+ # WHEN: pass a bad filename
+ self.router.send_response.reset_mock()
+ self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png')
+
+ # THEN: return a 404
+ self.router.send_response.assert_called_once_with(404)
+
+ # WHEN: a dangerous URL is passed
+ self.router.send_response.reset_mock()
+ self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png')
+
+ # THEN: return a 404
+ self.router.send_response.assert_called_once_with(404)
+
+ def serve_thumbnail_with_valid_params_test(self):
+ """
+ Test the serve_thumbnail routine with valid params
+ """
+ # GIVEN: Mocked send_header, send_response, end_headers and wfile
+ self.router.send_response = MagicMock()
+ self.router.send_header = MagicMock()
+ self.router.end_headers = MagicMock()
+ self.router.wfile = MagicMock()
+ mocked_image_manager = MagicMock()
+ Registry.create()
+ Registry().register('image_manager', mocked_image_manager)
+ file_name = 'another%20test/slide1.png'
+ full_path = os.path.normpath(os.path.join('thumbnails', file_name))
+ width = 120
+ height = 90
+ with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
+ patch('builtins.open', mock_open(read_data='123')), \
+ patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
+ patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
+ mocked_exists.return_value = True
+ mocked_image_to_byte.return_value = '123'
+ mocked_location.get_section_data_path.return_value = ''
+
+ # WHEN: pass good controller and filename
+ result = self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name)
+
+ # THEN: a file should be returned
+ self.assertEqual(self.router.send_header.call_count, 1, 'One header')
+ self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
+ self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
+ mocked_exists.assert_called_with(urllib.parse.unquote(full_path))
+ self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once')
+ mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'),
+ 'slide1.png', None, '120x90')
+ mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), 'slide1.png', '120x90')
=== added file 'tests/resources/presentations/test.ppt'
Binary files tests/resources/presentations/test.ppt 1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.ppt 2014-07-15 10:08:10 +0000 differ
=== added file 'tests/resources/presentations/test.pptx'
Binary files tests/resources/presentations/test.pptx 1970-01-01 00:00:00 +0000 and tests/resources/presentations/test.pptx 2014-07-15 10:08:10 +0000 differ
Follow ups