openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #22742
[Merge] lp:~tomasgroth/openlp/better-remote into lp:openlp
Tomas Groth has proposed merging lp:~tomasgroth/openlp/better-remote into lp:openlp.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/better-remote/+merge/211631
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)
- 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)
- 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/211631
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/better-remote into lp:openlp.
=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py 2013-12-24 08:56:50 +0000
+++ openlp/core/common/applocation.py 2014-03-18 21:03:35 +0000
@@ -76,8 +76,8 @@
return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0])
elif dir_type == AppLocation.PluginsDir:
app_path = os.path.abspath(os.path.split(sys.argv[0])[0])
- return get_frozen_path(os.path.join(app_path, 'plugins'),
- os.path.join(os.path.split(openlp.__file__)[0], 'plugins'))
+ return os.path.normpath(get_frozen_path(os.path.join(app_path, 'plugins'),
+ os.path.join(os.path.split(openlp.__file__)[0], 'plugins')))
elif dir_type == AppLocation.VersionDir:
return get_frozen_path(os.path.abspath(os.path.split(sys.argv[0])[0]), os.path.split(openlp.__file__)[0])
elif dir_type == AppLocation.LanguageDir:
=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py 2013-12-24 08:56:50 +0000
+++ openlp/core/common/settings.py 2014-03-18 21:03:35 +0000
@@ -386,7 +386,7 @@
"""
if self.group():
key = self.group() + '/' + key
- return Settings.__default_settings__[key]
+ return Settings.__default_settings__.get(key, '')
def remove_obsolete_settings(self):
"""
@@ -424,9 +424,9 @@
"""
# if group() is not empty the group has not been specified together with the key.
if self.group():
- default_value = Settings.__default_settings__[self.group() + '/' + key]
+ default_value = Settings.__default_settings__.get(self.group() + '/' + key, '')
else:
- default_value = Settings.__default_settings__[key]
+ default_value = Settings.__default_settings__.get(key, '')
setting = super(Settings, self).value(key, default_value)
return self._convert_value(setting, default_value)
=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 2014-03-11 18:58:49 +0000
+++ openlp/core/lib/__init__.py 2014-03-18 21:03:35 +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 2013-12-28 21:33:38 +0000
+++ openlp/core/lib/imagemanager.py 2014-03-18 21:03:35 +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.
@@ -128,6 +129,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
@@ -218,13 +228,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)
@@ -245,12 +255,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
@@ -265,12 +275,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
@@ -280,14 +290,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()):
@@ -316,7 +326,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-03-16 21:25:23 +0000
+++ openlp/core/lib/serviceitem.py 2014-03-18 21:03:35 +0000
@@ -108,6 +108,16 @@
``CanAutoStartForLive``
The capability to ignore the do not play if display blank flag.
+ ``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
@@ -125,6 +135,9 @@
CanWordSplit = 14
HasBackgroundAudio = 15
CanAutoStartForLive = 16
+ HasDisplayTitle = 17
+ HasNotes = 18
+ HasThumbnails = 19
class ServiceItem(RegistryProperties):
@@ -303,7 +316,7 @@
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.
@@ -317,7 +330,8 @@
The command of/for the slide.
"""
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):
@@ -362,7 +376,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, serviceitem, path=None):
@@ -434,7 +449,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-03-16 21:25:23 +0000
+++ openlp/core/ui/listpreviewwidget.py 2014-03-18 21:03:35 +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-03-16 21:25:23 +0000
+++ openlp/core/ui/servicemanager.py 2014-03-18 21:03:35 +0000
@@ -1267,10 +1267,17 @@
tree_widget_item.setToolTip(0, '<br>'.join(tips))
tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
tree_widget_item.setSelected(item['selected'])
- # 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()):
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-03-13 20:08:47 +0000
+++ openlp/core/ui/slidecontroller.py 2014-03-18 21:03:35 +0000
@@ -850,12 +850,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()
@@ -1025,10 +1030,12 @@
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()
+ if self.type_prefix == 'live':
+ Registry().execute('websock_send', '')
def on_slide_change(self, row):
"""
@@ -1038,7 +1045,7 @@
"""
self.preview_widget.change_slide(row)
self.update_preview()
- Registry().execute('slidecontroller_%s_changed' % self.type_prefix, row)
+ self.selected_row = row
def update_preview(self):
"""
=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py 2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py 2014-03-18 21:03:35 +0000
@@ -61,7 +61,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__)
@@ -253,6 +253,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/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py 2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py 2014-03-18 21:03:35 +0000
@@ -27,22 +27,24 @@
# 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
import logging
+from .ppinterface import constants
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 .presentationcontroller import PresentationController, PresentationDocument
-
log = logging.getLogger(__name__)
@@ -83,6 +85,8 @@
log.debug('start_process')
if not self.process:
self.process = Dispatch('PowerPoint.Application')
+ self.events = PowerpointEvents(self.process)
+ self.events.controller = self
self.process.Visible = True
self.process.WindowState = 2
@@ -135,6 +139,7 @@
return False
self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
self.create_thumbnails()
+ self.create_titles_and_notes()
return True
def create_thumbnails(self):
@@ -319,6 +324,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 _get_text_from_shapes(shapes):
"""
@@ -327,8 +355,33 @@
: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 == constants.ppPlaceholderBody:
+ if shape.HasTextFrame and shape.TextFrame.HasText:
+ text += shape.TextFrame.TextRange.Text + '\n'
return text
+
+if os.name == "nt":
+ ppE = win32com.client.getevents("PowerPoint.Application")
+
+ class PowerpointEvents(ppE):
+ def OnSlideShowBegin(self, hwnd):
+ #print("SS Begin")
+ return
+
+ def OnSlideShowEnd(self, pres):
+ #print("SS End")
+ return
+
+ def OnSlideShowNextSlide(self, hwnd):
+ Registry().execute('slidecontroller_live_change', hwnd.View.CurrentShowPosition - 1)
+ #print('Slide change:',hwnd.View.CurrentShowPosition)
+ return
+
+ def OnSlideShowOnNext(self, hwnd):
+ #print("SS Advance")
+ return
+
+ def OnSlideShowOnPrevious(self, hwnd):
+ #print("SS GoBack")
+ return
=== added file 'openlp/plugins/presentations/lib/ppinterface.py'
--- openlp/plugins/presentations/lib/ppinterface.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/ppinterface.py 2014-03-18 21:03:35 +0000
@@ -0,0 +1,330 @@
+# -*- 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 #
+###############################################################################
+"""
+These declarations have been extracted from the interface file created by makepy
+"""
+class constants:
+ ppPlaceholderBody =2 # from enum PpPlaceholderType
+
+import os
+if os.name=='nt':
+ import pythoncom, pywintypes
+ from win32com.client import Dispatch,DispatchBaseClass,CoClassBaseClass, \
+ CLSIDToClass
+ from pywintypes import IID
+ import win32com
+
+
+ # The following 3 lines may need tweaking for the particular server
+ # Candidates are pythoncom.Missing, .Empty and .ArgNotFound
+ defaultNamedOptArg=pythoncom.Empty
+ defaultNamedNotOptArg=pythoncom.Empty
+ defaultUnnamedArg=pythoncom.Empty
+
+
+ CLSID = IID('{91493440-5A91-11CF-8700-00AA0060263B}')
+ MajorVersion = 2
+ MinorVersion = 11
+ LibraryFlags = 8
+ LCID = 0x0
+
+ class EApplication:
+ CLSID = CLSID_Sink = IID('{914934C2-5A91-11CF-8700-00AA0060263B}')
+ coclass_clsid = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+ _public_methods_ = [] # For COM Server support
+ _dispid_to_func_ = {
+ 2029 : "OnProtectedViewWindowActivate",
+ 2015 : "OnPresentationPrint",
+ 2013 : "OnSlideShowNextSlide",
+ 2011 : "OnSlideShowBegin",
+ 2001 : "OnWindowSelectionChange",
+ 2005 : "OnPresentationSave",
+ 2020 : "OnAfterNewPresentation",
+ 2014 : "OnSlideShowEnd",
+ 2028 : "OnProtectedViewWindowBeforeClose",
+ 2025 : "OnPresentationBeforeClose",
+ 2018 : "OnPresentationBeforeSave",
+ 2010 : "OnWindowDeactivate",
+ 2021 : "OnAfterPresentationOpen",
+ 2027 : "OnProtectedViewWindowBeforeEdit",
+ 2026 : "OnProtectedViewWindowOpen",
+ 2023 : "OnSlideShowOnNext",
+ 2012 : "OnSlideShowNextBuild",
+ 2002 : "OnWindowBeforeRightClick",
+ 2030 : "OnProtectedViewWindowDeactivate",
+ 2016 : "OnSlideSelectionChanged",
+ 2004 : "OnPresentationClose",
+ 2017 : "OnColorSchemeChanged",
+ 2019 : "OnSlideShowNextClick",
+ 2006 : "OnPresentationOpen",
+ 2003 : "OnWindowBeforeDoubleClick",
+ 2031 : "OnPresentationCloseFinal",
+ 2032 : "OnAfterDragDropOnSlide",
+ 2033 : "OnAfterShapeSizeChange",
+ 2009 : "OnWindowActivate",
+ 2022 : "OnPresentationSync",
+ 2007 : "OnNewPresentation",
+ 2024 : "OnSlideShowOnPrevious",
+ 2008 : "OnPresentationNewSlide",
+ }
+
+ def __init__(self, oobj = None):
+ if oobj is None:
+ self._olecp = None
+ else:
+ import win32com.server.util
+ from win32com.server.policy import EventHandlerPolicy
+ cpc=oobj._oleobj_.QueryInterface(pythoncom.IID_IConnectionPointContainer)
+ cp=cpc.FindConnectionPoint(self.CLSID_Sink)
+ cookie=cp.Advise(win32com.server.util.wrap(self, usePolicy=EventHandlerPolicy))
+ self._olecp,self._olecp_cookie = cp,cookie
+ def __del__(self):
+ try:
+ self.close()
+ except pythoncom.com_error:
+ pass
+ def close(self):
+ if self._olecp is not None:
+ cp,cookie,self._olecp,self._olecp_cookie = self._olecp,self._olecp_cookie,None,None
+ cp.Unadvise(cookie)
+ def _query_interface_(self, iid):
+ import win32com.server.util
+ if iid==self.CLSID_Sink: return win32com.server.util.wrap(self)
+
+ class _Application(DispatchBaseClass):
+ CLSID = IID('{91493442-5A91-11CF-8700-00AA0060263B}')
+ coclass_clsid = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+
+ def Activate(self):
+ return self._oleobj_.InvokeTypes(2033, LCID, 1, (24, 0), (),)
+
+ # Result is of type FileDialog
+ # The method FileDialog is actually a property, but must be used as a method to correctly pass the arguments
+ def FileDialog(self, Type=defaultNamedNotOptArg):
+ ret = self._oleobj_.InvokeTypes(2045, LCID, 2, (9, 0), ((3, 1),),Type
+ )
+ if ret is not None:
+ ret = Dispatch(ret, 'FileDialog', '{000C0362-0000-0000-C000-000000000046}')
+ return ret
+
+ def GetOptionFlag(self, Option=defaultNamedNotOptArg, Persist=False):
+ return self._oleobj_.InvokeTypes(2043, LCID, 1, (11, 0), ((3, 1), (11, 49)),Option
+ , Persist)
+
+ def Help(self, HelpFile='vbapp10.chm', ContextID=0):
+ return self._ApplyTypes_(2020, 1, (24, 32), ((8, 49), (3, 49)), 'Help', None,HelpFile
+ , ContextID)
+
+ def LaunchPublishSlidesDialog(self, SlideLibraryUrl=defaultNamedNotOptArg):
+ return self._oleobj_.InvokeTypes(2054, LCID, 1, (24, 0), ((8, 1),),SlideLibraryUrl
+ )
+
+ def LaunchSendToPPTDialog(self, SlideUrls=defaultNamedNotOptArg):
+ return self._oleobj_.InvokeTypes(2055, LCID, 1, (24, 0), ((16396, 1),),SlideUrls
+ )
+
+ # Result is of type Theme
+ def OpenThemeFile(self, themeFileName=defaultNamedNotOptArg):
+ ret = self._oleobj_.InvokeTypes(2069, LCID, 1, (9, 0), ((8, 1),),themeFileName
+ )
+ if ret is not None:
+ ret = Dispatch(ret, 'OpenThemeFile', '{D9D60EB3-D4B4-4991-9C16-75585B3346BB}')
+ return ret
+
+ def PPFileDialog(self, Type=defaultNamedNotOptArg):
+ ret = self._oleobj_.InvokeTypes(2023, LCID, 1, (13, 0), ((3, 1),),Type
+ )
+ if ret is not None:
+ # See if this IUnknown is really an IDispatch
+ try:
+ ret = ret.QueryInterface(pythoncom.IID_IDispatch)
+ except pythoncom.error:
+ return ret
+ ret = Dispatch(ret, 'PPFileDialog', None)
+ return ret
+
+ def Quit(self):
+ return self._oleobj_.InvokeTypes(2021, LCID, 1, (24, 0), (),)
+
+ def Run(self, *args):
+ return self._get_good_object_(self._oleobj_.Invoke(*((2022,0,1,1)+args)),'Run')
+
+ def SetOptionFlag(self, Option=defaultNamedNotOptArg, State=defaultNamedNotOptArg, Persist=False):
+ return self._oleobj_.InvokeTypes(2044, LCID, 1, (24, 0), ((3, 1), (11, 1), (11, 49)),Option
+ , State, Persist)
+
+ def SetPerfMarker(self, Marker=defaultNamedNotOptArg):
+ return self._oleobj_.InvokeTypes(2051, LCID, 1, (24, 0), ((3, 1),),Marker
+ )
+
+ def StartNewUndoEntry(self):
+ return self._oleobj_.InvokeTypes(2067, LCID, 1, (24, 0), (),)
+
+ _prop_map_get_ = {
+ "Active": (2032, 2, (3, 0), (), "Active", None),
+ "ActiveEncryptionSession": (2058, 2, (3, 0), (), "ActiveEncryptionSession", None),
+ # Method 'ActivePresentation' returns object of type 'Presentation'
+ "ActivePresentation": (2005, 2, (13, 0), (), "ActivePresentation", '{91493444-5A91-11CF-8700-00AA0060263B}'),
+ "ActivePrinter": (2016, 2, (8, 0), (), "ActivePrinter", None),
+ # Method 'ActiveProtectedViewWindow' returns object of type 'ProtectedViewWindow'
+ "ActiveProtectedViewWindow": (2064, 2, (9, 0), (), "ActiveProtectedViewWindow", '{BA72E55A-4FF5-48F4-8215-5505F990966F}'),
+ # Method 'ActiveWindow' returns object of type 'DocumentWindow'
+ "ActiveWindow": (2004, 2, (9, 0), (), "ActiveWindow", '{91493457-5A91-11CF-8700-00AA0060263B}'),
+ # Method 'AddIns' returns object of type 'AddIns'
+ "AddIns": (2018, 2, (9, 0), (), "AddIns", '{91493460-5A91-11CF-8700-00AA0060263B}'),
+ # Method 'AnswerWizard' returns object of type 'AnswerWizard'
+ "AnswerWizard": (2034, 2, (9, 0), (), "AnswerWizard", '{000C0360-0000-0000-C000-000000000046}'),
+ # Method 'Assistance' returns object of type 'IAssistance'
+ "Assistance": (2057, 2, (9, 0), (), "Assistance", '{4291224C-DEFE-485B-8E69-6CF8AA85CB76}'),
+ # Method 'Assistant' returns object of type 'Assistant'
+ "Assistant": (2010, 2, (9, 0), (), "Assistant", '{000C0322-0000-0000-C000-000000000046}'),
+ # Method 'AutoCorrect' returns object of type 'AutoCorrect'
+ "AutoCorrect": (2052, 2, (9, 0), (), "AutoCorrect", '{914934ED-5A91-11CF-8700-00AA0060263B}'),
+ "AutomationSecurity": (2047, 2, (3, 0), (), "AutomationSecurity", None),
+ "Build": (2013, 2, (8, 0), (), "Build", None),
+ # Method 'COMAddIns' returns object of type 'COMAddIns'
+ "COMAddIns": (2035, 2, (9, 0), (), "COMAddIns", '{000C0339-0000-0000-C000-000000000046}'),
+ "Caption": (2009, 2, (8, 0), (), "Caption", None),
+ "ChartDataPointTrack": (2070, 2, (11, 0), (), "ChartDataPointTrack", None),
+ # Method 'CommandBars' returns object of type 'CommandBars'
+ "CommandBars": (2007, 2, (13, 0), (), "CommandBars", '{55F88893-7708-11D1-ACEB-006008961DA5}'),
+ "Creator": (2017, 2, (3, 0), (), "Creator", None),
+ # Method 'DefaultWebOptions' returns object of type 'DefaultWebOptions'
+ "DefaultWebOptions": (2037, 2, (9, 0), (), "DefaultWebOptions", '{914934CD-5A91-11CF-8700-00AA0060263B}'),
+ "Dialogs": (2003, 2, (13, 0), (), "Dialogs", None),
+ "DisplayAlerts": (2049, 2, (3, 0), (), "DisplayAlerts", None),
+ "DisplayDocumentInformationPanel": (2056, 2, (11, 0), (), "DisplayDocumentInformationPanel", None),
+ "DisplayGridLines": (2046, 2, (3, 0), (), "DisplayGridLines", None),
+ "DisplayGuides": (2071, 2, (3, 0), (), "DisplayGuides", None),
+ "FeatureInstall": (2042, 2, (3, 0), (), "FeatureInstall", None),
+ # Method 'FileConverters' returns object of type 'FileConverters'
+ "FileConverters": (2059, 2, (9, 0), (), "FileConverters", '{92D41A50-F07E-4CA4-AF6F-BEF486AA4E6F}'),
+ # Method 'FileFind' returns object of type 'IFind'
+ "FileFind": (2012, 2, (9, 0), (), "FileFind", '{000C0337-0000-0000-C000-000000000046}'),
+ # Method 'FileSearch' returns object of type 'FileSearch'
+ "FileSearch": (2011, 2, (9, 0), (), "FileSearch", '{000C0332-0000-0000-C000-000000000046}'),
+ "FileValidation": (2068, 2, (3, 0), (), "FileValidation", None),
+ "HWND": (2031, 2, (3, 0), (), "HWND", None),
+ "Height": (2028, 2, (4, 0), (), "Height", None),
+ "IsSandboxed": (2065, 2, (11, 0), (), "IsSandboxed", None),
+ # Method 'LanguageSettings' returns object of type 'LanguageSettings'
+ "LanguageSettings": (2038, 2, (9, 0), (), "LanguageSettings", '{000C0353-0000-0000-C000-000000000046}'),
+ "Left": (2025, 2, (4, 0), (), "Left", None),
+ "Marker": (2041, 2, (13, 0), (), "Marker", None),
+ # Method 'MsoDebugOptions' returns object of type 'MsoDebugOptions'
+ "MsoDebugOptions": (2039, 2, (9, 0), (), "MsoDebugOptions", '{000C035A-0000-0000-C000-000000000046}'),
+ "Name": (0, 2, (8, 0), (), "Name", None),
+ # Method 'NewPresentation' returns object of type 'NewFile'
+ "NewPresentation": (2048, 2, (9, 0), (), "NewPresentation", '{000C0936-0000-0000-C000-000000000046}'),
+ "OperatingSystem": (2015, 2, (8, 0), (), "OperatingSystem", None),
+ # Method 'Options' returns object of type 'Options'
+ "Options": (2053, 2, (9, 0), (), "Options", '{914934EE-5A91-11CF-8700-00AA0060263B}'),
+ "Path": (2008, 2, (8, 0), (), "Path", None),
+ # Method 'Presentations' returns object of type 'Presentations'
+ "Presentations": (2001, 2, (9, 0), (), "Presentations", '{91493462-5A91-11CF-8700-00AA0060263B}'),
+ "ProductCode": (2036, 2, (8, 0), (), "ProductCode", None),
+ # Method 'ProtectedViewWindows' returns object of type 'ProtectedViewWindows'
+ "ProtectedViewWindows": (2063, 2, (9, 0), (), "ProtectedViewWindows", '{BA72E559-4FF5-48F4-8215-5505F990966F}'),
+ # Method 'ResampleMediaTasks' returns object of type 'ResampleMediaTasks'
+ "ResampleMediaTasks": (2066, 2, (9, 0), (), "ResampleMediaTasks", '{BA72E554-4FF5-48F4-8215-5505F990966F}'),
+ "ShowStartupDialog": (2050, 2, (3, 0), (), "ShowStartupDialog", None),
+ "ShowWindowsInTaskbar": (2040, 2, (3, 0), (), "ShowWindowsInTaskbar", None),
+ # Method 'SlideShowWindows' returns object of type 'SlideShowWindows'
+ "SlideShowWindows": (2006, 2, (9, 0), (), "SlideShowWindows", '{91493456-5A91-11CF-8700-00AA0060263B}'),
+ # Method 'SmartArtColors' returns object of type 'SmartArtColors'
+ "SmartArtColors": (2062, 2, (9, 0), (), "SmartArtColors", '{000C03CD-0000-0000-C000-000000000046}'),
+ # Method 'SmartArtLayouts' returns object of type 'SmartArtLayouts'
+ "SmartArtLayouts": (2060, 2, (9, 0), (), "SmartArtLayouts", '{000C03C9-0000-0000-C000-000000000046}'),
+ # Method 'SmartArtQuickStyles' returns object of type 'SmartArtQuickStyles'
+ "SmartArtQuickStyles": (2061, 2, (9, 0), (), "SmartArtQuickStyles", '{000C03CB-0000-0000-C000-000000000046}'),
+ "Top": (2026, 2, (4, 0), (), "Top", None),
+ # Method 'VBE' returns object of type 'VBE'
+ "VBE": (2019, 2, (9, 0), (), "VBE", '{0002E166-0000-0000-C000-000000000046}'),
+ "Version": (2014, 2, (8, 0), (), "Version", None),
+ "Visible": (2030, 2, (3, 0), (), "Visible", None),
+ "Width": (2027, 2, (4, 0), (), "Width", None),
+ "WindowState": (2029, 2, (3, 0), (), "WindowState", None),
+ # Method 'Windows' returns object of type 'DocumentWindows'
+ "Windows": (2002, 2, (9, 0), (), "Windows", '{91493455-5A91-11CF-8700-00AA0060263B}'),
+ }
+ _prop_map_put_ = {
+ "AutomationSecurity": ((2047, LCID, 4, 0),()),
+ "Caption": ((2009, LCID, 4, 0),()),
+ "ChartDataPointTrack": ((2070, LCID, 4, 0),()),
+ "DisplayAlerts": ((2049, LCID, 4, 0),()),
+ "DisplayDocumentInformationPanel": ((2056, LCID, 4, 0),()),
+ "DisplayGridLines": ((2046, LCID, 4, 0),()),
+ "DisplayGuides": ((2071, LCID, 4, 0),()),
+ "FeatureInstall": ((2042, LCID, 4, 0),()),
+ "FileValidation": ((2068, LCID, 4, 0),()),
+ "Height": ((2028, LCID, 4, 0),()),
+ "Left": ((2025, LCID, 4, 0),()),
+ "ShowStartupDialog": ((2050, LCID, 4, 0),()),
+ "ShowWindowsInTaskbar": ((2040, LCID, 4, 0),()),
+ "Top": ((2026, LCID, 4, 0),()),
+ "Visible": ((2030, LCID, 4, 0),()),
+ "Width": ((2027, LCID, 4, 0),()),
+ "WindowState": ((2029, LCID, 4, 0),()),
+ }
+ # Default property for this class is 'Name'
+ def __call__(self):
+ return self._ApplyTypes_(*(0, 2, (8, 0), (), "Name", None))
+ def __str__(self, *args):
+ return str(self.__call__(*args))
+ def __int__(self, *args):
+ return int(self.__call__(*args))
+ def __iter__(self):
+ "Return a Python iterator for this object"
+ try:
+ ob = self._oleobj_.InvokeTypes(-4,LCID,3,(13, 10),())
+ except pythoncom.error:
+ raise TypeError("This object does not support enumeration")
+ return win32com.client.util.Iterator(ob, None)
+
+ # This CoClass is known by the name 'PowerPoint.Application.15'
+ class Application(CoClassBaseClass): # A CoClass
+ CLSID = IID('{91493441-5A91-11CF-8700-00AA0060263B}')
+ coclass_sources = [
+ EApplication,
+ ]
+ default_source = EApplication
+ coclass_interfaces = [
+ _Application,
+ ]
+ default_interface = _Application
+
+
+ CLSIDToClassMap = {
+ '{91493441-5A91-11CF-8700-00AA0060263B}' : Application,
+ '{91493442-5A91-11CF-8700-00AA0060263B}' : _Application,
+ '{914934C2-5A91-11CF-8700-00AA0060263B}' : EApplication,
+ }
+
+ CLSIDToPackageMap = {}
+ win32com.client.CLSIDToClass.RegisterCLSIDsFromDict( CLSIDToClassMap )
=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py 2014-03-08 21:23:47 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py 2014-03-18 21:03:35 +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
@@ -124,14 +129,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()
@@ -151,6 +156,70 @@
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)
+ #print("slide count: " + str(len(nodes)))
+ # 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-03-18 21:03:35 +0000 differ
=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py 2014-03-17 07:14:51 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py 2014-03-18 21:03:35 +0000
@@ -292,6 +292,49 @@
"""
return ''
+ def get_titles_and_notes(self):
+ """
+ Reads the titles from the titles file and
+ the notes files and returns the contents
+ in a 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):
"""
@@ -426,3 +469,25 @@
def close_presentation(self):
pass
+<<<<<<< TREE
+=======
+
+ def _get_plugin_manager(self):
+ """
+ Adds the plugin manager to the class dynamically
+ """
+ if not hasattr(self, '_plugin_manager'):
+ self._plugin_manager = Registry().get('plugin_manager')
+ return self._plugin_manager
+
+ plugin_manager = property(_get_plugin_manager)
+
+
+class TextType(object):
+ """
+ Type Enumeration for Types of Text to request
+ """
+ Title = 0
+ SlideText = 1
+ Notes = 2
+>>>>>>> MERGE-SOURCE
=== added file 'openlp/plugins/remotes/html/WebSocketEvents.js'
--- openlp/plugins/remotes/html/WebSocketEvents.js 1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/WebSocketEvents.js 2014-03-18 21:03:35 +0000
@@ -0,0 +1,113 @@
+/******************************************************************************
+ * 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 *
+ * --------------------------------------------------------------------------- *
+ * 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 *
+ ******************************************************************************/
+
+/* Thanks to Ismael Celis for the original idea */
+
+var wsEventEngine = function(url, polling_function, polling_interval)
+{
+ this.polling_handle = null;
+ this.polling_interval = polling_interval;
+ this.polling_function = polling_function;
+ this.retry_handle = null;
+ this.callbacks = {};
+
+ this.fallback = function(){
+ this.kill_polling();
+ if(this.polling_function)
+ this.polling_handle = window.setInterval(this.polling_function, this.polling_interval);
+ this.kill_retries();
+ var theEngine = this;
+ this.retry_handle = window.setInterval(function(){theEngine.setup();}, 10000);
+ }
+
+ this.kill_polling = function(){
+ if(this.polling_handle)
+ window.clearInterval(this.polling_handle);
+ this.polling_handle = null;
+ }
+
+ this.kill_retries = function(){
+ if(this.retry_handle)
+ window.clearInterval(this.retry_handle);
+ }
+
+ this.bind = function(event_name, callback){
+ this.callbacks[event_name] = this.callbacks[event_name] || [];
+ this.callbacks[event_name].push(callback);
+ return this;
+ }
+
+ this.send = function(event_name, event_data){
+ var payload = JSON.stringify({ event: event_name, data: event_data });
+ this.websocket.send(payload);
+ return this;
+ }
+
+ this.dispatch = function(event_name, message){
+ var chain = this.callbacks[event_name];
+ if(typeof chain == 'undefined') return; // no callbacks
+ for(var i = 0; i < chain.length; i++)
+ chain[i](message);
+ }
+
+ this.setup = function(){
+ this.websocket = new WebSocket(url);
+ this.websocket.engine = this;
+
+ this.websocket.onmessage = function(websocket_msg){
+ if(this.engine.polling_function)
+ this.engine.polling_function();
+ if( websocket_msg.data.length > 0 ){
+ try{
+ var json = JSON.parse(websocket_msg.data);
+ this.engine.dispatch(json.event, json.data);
+ }
+ catch(err){
+ }
+ }
+ }
+
+ this.websocket.onclose = function(){
+ this.engine.dispatch('close', null);
+ this.engine.fallback();
+ }
+
+ this.websocket.onopen = function(){
+ this.engine.dispatch('open', null);
+ this.engine.kill_polling();
+ this.engine.kill_retries();
+ }
+
+ }
+
+ if('WebSocket' in window){
+ this.setup();
+ }
+ else{
+ this.fallback();
+ }
+
+}
\ No newline at end of file
=== 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-03-18 21:03:35 +0000
@@ -35,6 +35,7 @@
<link rel="stylesheet" href="/files/openlp.css" />
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
<script type="text/javascript" src="/files/jquery.js"></script>
+ <script type="text/javascript" src="/files/WebSocketEvents.js"></script>
<script type="text/javascript" src="/files/openlp.js"></script>
<script type="text/javascript" src="/files/jquery.mobile.js"></script>
<script type="text/javascript">
@@ -120,6 +121,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-03-18 21:03:35 +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
@@ -363,5 +398,6 @@
$("#search").live("pageinit", function (event) {
OpenLP.getSearchablePlugins();
});
-setInterval("OpenLP.pollServer();", 500);
-OpenLP.pollServer();
+//setInterval("OpenLP.pollServer();", 30000);
+//OpenLP.pollServer();
+var ws = new wsEventEngine("ws://" + window.location.hostname + ":8888/Test", OpenLP.pollServer, 500);
=== modified file 'openlp/plugins/remotes/lib/__init__.py'
--- openlp/plugins/remotes/lib/__init__.py 2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/lib/__init__.py 2014-03-18 21:03:35 +0000
@@ -30,5 +30,6 @@
from .remotetab import RemoteTab
from .httprouter import HttpRouter
from .httpserver import OpenLPServer
+from .websocket import WebSocketManager
-__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']
+__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter','WebSocketManager']
=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py 2014-03-16 21:25:23 +0000
+++ openlp/plugins/remotes/lib/httprouter.py 2014-03-18 21:03:35 +0000
@@ -124,8 +124,13 @@
from mako.template import Template
from PyQt4 import QtCore
+<<<<<<< TREE
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate
from openlp.core.lib import PluginStatus, StringContent, image_to_byte
+=======
+from openlp.core.common import Registry, AppLocation, Settings, translate
+from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities
+>>>>>>> MERGE-SOURCE
log = logging.getLogger(__name__)
FILE_TYPES = {
@@ -159,6 +164,7 @@
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
('^/(main)$', {'function': self.serve_file, 'secure': False}),
(r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
+ (r'^/(.*)/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}),
@@ -334,7 +340,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):
@@ -388,6 +395,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']
+ 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.
@@ -475,9 +511,21 @@
item['html'] = str(frame['html'])
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:
@@ -601,3 +649,55 @@
item_id = plugin.media_item.create_item_from_id(id)
plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
self.do_http_success()
+<<<<<<< TREE
+=======
+
+ def _get_service_manager(self):
+ """
+ Adds the service manager to the class dynamically
+ """
+ if not hasattr(self, '_service_manager'):
+ self._service_manager = Registry().get('service_manager')
+ return self._service_manager
+
+ service_manager = property(_get_service_manager)
+
+ def _get_live_controller(self):
+ """
+ Adds the live controller to the class dynamically
+ """
+ if not hasattr(self, '_live_controller'):
+ self._live_controller = Registry().get('live_controller')
+ return self._live_controller
+
+ live_controller = property(_get_live_controller)
+
+ def _get_plugin_manager(self):
+ """
+ Adds the plugin manager to the class dynamically
+ """
+ if not hasattr(self, '_plugin_manager'):
+ self._plugin_manager = Registry().get('plugin_manager')
+ return self._plugin_manager
+
+ plugin_manager = property(_get_plugin_manager)
+
+ def _get_alerts_manager(self):
+ """
+ Adds the alerts manager to the class dynamically
+ """
+ if not hasattr(self, '_alerts_manager'):
+ self._alerts_manager = Registry().get('alerts_manager')
+ return self._alerts_manager
+
+ alerts_manager = property(_get_alerts_manager)
+
+ def _get_image_manager(self):
+ """
+ Adds the image manager to the class dynamically
+ """
+ if not hasattr(self, '_image_manager'):
+ self._image_manager = Registry().get('image_manager')
+ return self._image_manager
+
+ image_manager = property(_get_image_manager)>>>>>>> MERGE-SOURCE
=== added file 'openlp/plugins/remotes/lib/websocket.py'
--- openlp/plugins/remotes/lib/websocket.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/lib/websocket.py 2014-03-18 21:03:35 +0000
@@ -0,0 +1,330 @@
+# -*- 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 #
+###############################################################################
+
+"""
+Simple implementation of RFC 6455 for websocket protocol in a very simple and focused manner, just for the purposes
+of this application
+"""
+
+import logging
+import re
+import socketserver
+import threading
+import time
+import socket
+import base64
+import uuid
+
+from base64 import b64encode
+from hashlib import sha1
+
+HOST, PORT = '', 8888
+WEB_SOCKETS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.encode('utf-8')
+WEB_SOCKETS_RESPONSE_TEMPLATE = (
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Connection: Upgrade',
+ 'Sec-WebSocket-Accept: {key}',
+ 'Upgrade: websocket',
+ '',
+ '',
+)
+WEB_SOCKETS_HANDSHAKE_ERROR = 'Error: Handshake'.encode('utf-8')
+WEB_SOCKET_CLIENT_HEADERS = (
+ "GET / HTTP/1.1",
+ "Upgrade: websocket",
+ "Connection: Upgrade",
+ "Host: {host}:{port}",
+ "Origin: null",
+ "Sec-WebSocket-Key: {key}",
+ "Sec-WebSocket-Version: 13",
+ "",
+ "",
+)
+
+
+class ThreadedWebSocketHandler(socketserver.BaseRequestHandler):
+ """
+ ThreadedWebSocketHandler implements the upgrade handshake and continues to serve the socket
+ """
+ def handle(self):
+ """
+ Called once per connection, the connection will not be added to the list of clients
+ until the handshake has succeeded
+ """
+ has_upgraded = False
+ data_buffer = ''
+ while True:
+ data_string = ''
+ data_received = ''
+ try:
+ data_received = self.request.recv(1024)
+ except Exception as e:
+ #print(self.client_address, e.errno, e.strerror)
+ if e.errno == 10053 or e.errno == 10054:
+ self.server.remove_client(self)
+ break
+ if len(data_received) > 0:
+ #print(" data_received: ", data_received)
+ if has_upgraded:
+ data_string = ThreadedWebSocketHandler.decode_websocket_message(data_received)
+ else:
+ data_string = data_received.decode('utf-8', 'ignore')
+ if len(data_string) > 0:
+ #print(" from: ", self.client_address, " data: ", data_string, " upgraded: ", has_upgraded)
+ if not has_upgraded:
+ data_buffer += data_string
+ #print("x", data_buffer, "x")
+ if data_buffer[0] != 'G':
+ #print("return error")
+ self.request.send(WEB_SOCKETS_HANDSHAKE_ERROR)
+ break
+ match = re.search('Sec-WebSocket-Key:\s+(.*?)[\n\r]+', data_buffer)
+ #print("match: ", match)
+ if match:
+ received_key = (match.groups()[0].strip()).encode('utf-8')
+ generated_key = sha1(received_key + WEB_SOCKETS_GUID).digest()
+ response_key = b64encode(generated_key).decode('utf-8')
+ response = ('\r\n'.join(WEB_SOCKETS_RESPONSE_TEMPLATE).format(key=response_key)).encode('utf-8')
+ #print(response)
+ self.request.send(response)
+ has_upgraded = True
+ data_buffer = ''
+ self.server.add_client(self)
+
+ @staticmethod
+ def decode_websocket_message(byte_array):
+ """
+ decode_websocket_message decodes the messages sent from a websocket client according to RFC 6455
+ :param byte_array: an array of bytes as received from the socket
+ :return: returns a string
+ """
+ data_length = byte_array[1] & 127
+ index_first_mask = 2
+ if data_length == 126:
+ index_first_mask = 4
+ elif data_length == 127:
+ index_first_mask = 10
+ masks = [m for m in byte_array[index_first_mask: index_first_mask + 4]]
+ index_first_data_byte = index_first_mask + 4
+ decoded_chars = []
+ index = index_first_data_byte
+ secondary_index = 0
+ while index < len(byte_array):
+ char = chr(byte_array[index] ^ masks[secondary_index % 4])
+ #print(char)
+ decoded_chars.append(char)
+ index += 1
+ secondary_index += 1
+ return ''.join(decoded_chars)
+
+ @staticmethod
+ def encode_websocket_message(message):
+ """
+ encode_websocket_message encodes a message prior to sending to a websocket client according to RFC 6455
+ :param message: string to be encoded
+ :return: the message encoded into a byte array
+ """
+ frame_head = bytearray(2)
+ frame_head[0] = ThreadedWebSocketHandler.set_bit(frame_head[0], 7)
+ frame_head[0] = ThreadedWebSocketHandler.set_bit(frame_head[0], 0)
+ assert(len(message) < 126)
+ frame_head[1] = len(message)
+ frame = frame_head + message.encode('utf-8')
+ return frame
+
+ @staticmethod
+ def decode_client_websocket_message(received_broadcast):
+ """
+ Helper to decode messages from the client side for testing purposes
+ :param received_broadcast: the byte array received from the server
+ :return: a decoded string
+ """
+ decoded_broadcast = ''
+ if received_broadcast[0] == 129:
+ for c in received_broadcast[2:]:
+ decoded_broadcast += chr(c)
+ return decoded_broadcast
+
+
+ @staticmethod
+ def set_bit(int_type, offset):
+ """
+ set_bit -- helper for bit operation
+ :param int_type: the original value
+ :param offset: which bit to set
+ :return: the modified value
+ """
+ return int_type | (1 << offset)
+
+ def finish(self):
+ """
+ finish is called when the connection is done
+ """
+ #print("finish:", self.client_address)
+# with self.server.lock:
+# self.server.remove_client(self)
+ pass
+
+
+class ThreadedWebSocketServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
+ """
+ ThreadedWebSocketServer overrides the standard implementation to add a client list
+ """
+ daemon_threads = True
+ allow_reuse_address = True
+
+ def __init__(self, host_port, handler):
+ super().__init__(host_port, handler)
+ self.clients = {}
+ self.lock = threading.Lock()
+
+ def add_client(self, client):
+ """
+ add_client inserts a reference to the client handler object into the server's list of clients
+ :param client: reference to the client handler
+ """
+ with self.lock:
+ self.clients[client.client_address] = client
+ #print("added: ", client.client_address)
+ #print(self.clients.keys())
+
+ def remove_client(self, client):
+ """
+ remove_client is called by the client handler when the client disconnects
+ :param client: reference to the client handler
+ """
+ with self.lock:
+ if client.client_address in self.clients.keys():
+ self.clients.pop(client.client_address)
+ #print("removed: ", client.client_address)
+
+ def send_to_all_clients(self, msg):
+ """
+ send_to_all_clients sends the same message to all the connected clients
+ :param msg: string to be sent to all connected clients
+ """
+ #print('send_to_all_clients')
+ #print(self.clients.keys())
+ with self.lock:
+ for client in self.clients.values():
+ #print("send_to:", client.client_address)
+ client.request.send(ThreadedWebSocketHandler.encode_websocket_message(msg))
+
+
+class WebSocketManager():
+ """
+ WebSocketManager implements the external interface to the WebSocket engine
+ """
+
+ def __init__(self):
+ self.server = None
+ self.server_thread = None
+
+ def start(self):
+ """
+ start
+ starts the WebSocket engine
+ """
+ self.server = ThreadedWebSocketServer((HOST, PORT), ThreadedWebSocketHandler)
+ self.server_thread = socketserver.threading.Thread(target=self.server.serve_forever)
+ self.server_thread.start()
+ #print("started the WebSocket server")
+
+ def stop(self):
+ """
+ stop
+ stops the WebSocket engine
+ """
+ self.server.shutdown()
+ self.server.server_close()
+ #print("stopped the WebSocket server")
+
+ def send(self, msg):
+ """
+ sends a message to all clients via the websocket server
+ :param msg: string to send
+ """
+ #print(self.server.clients.keys())
+ self.server.send_to_all_clients(msg)
+
+if __name__ == "__main__":
+ # The following code is helpful to test the server using a browser
+ # Just paste the following code into an html file
+ #<html>
+ #<head>
+ #<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+ #</head>
+ #<body>
+ # <div id="results">start:</div>
+ #</body>
+ #<script type="text/javascript">
+ # appendMessage("testing...");
+ # var ws = new WebSocket('ws://localhost:8888/Pres')
+ # ws.onmessage = function(e){
+ # appendMessage(e.data)
+ # }
+ # ws.onopen = function(){
+ # appendMessage("open");
+ # this.send("test send");
+ # }
+ # ws.onclose = function(){
+ # appendMessage("closed");
+ # }
+ # function appendMessage(str)
+ # {
+ # $("#results").html($("#results").html() + "<br />" + str);
+ # }
+ #</script>
+ #</html>
+
+ manager = WebSocketManager()
+ manager.start()
+ # Create a socket (SOCK_STREAM means a TCP socket)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Fake a handshake
+ uid = uuid.uuid4()
+ key = base64.encodebytes(uid.bytes).strip()
+ data = ('\r\n'.join(WEB_SOCKET_CLIENT_HEADERS).format(host='localhost', port='8888', key=key)).encode('utf-8')
+ received = None
+ try:
+ # Connect to server and send data
+ sock.connect(('localhost', PORT))
+ sock.send(data)
+ received = sock.recv(1024)
+ time.sleep(5)
+ manager.send("broadcast")
+ print("received: ", ThreadedWebSocketHandler.decode_client_websocket_message(sock.recv(1024)))
+ time.sleep(2)
+ manager.send("\r\njust before kill")
+ print("received: ", ThreadedWebSocketHandler.decode_client_websocket_message(sock.recv(1024)))
+ time.sleep(2)
+ finally:
+ sock.close()
+ manager.stop()
+
=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py 2013-12-24 08:56:50 +0000
+++ openlp/plugins/remotes/remoteplugin.py 2014-03-18 21:03:35 +0000
@@ -31,7 +31,8 @@
import time
from openlp.core.lib import Plugin, StringContent, translate, build_icon
-from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
+from openlp.core.common import Registry
+from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer, WebSocketManager
log = logging.getLogger(__name__)
@@ -59,6 +60,7 @@
self.icon = build_icon(self.icon_path)
self.weight = -1
self.server = None
+ self.websocketserver = None
def initialise(self):
"""
@@ -67,6 +69,9 @@
log.debug('initialise')
super(RemotesPlugin, self).initialise()
self.server = OpenLPServer()
+ self.websocketserver = WebSocketManager()
+ self.websocketserver.start()
+ Registry().register_function('websock_send', self.websocketserver.send)
def finalise(self):
"""
@@ -77,6 +82,9 @@
if self.server:
self.server.stop_server()
self.server = None
+ if self.websocketserver:
+ self.websocketserver.stop()
+ self.websocketserver = None
def about(self):
"""
=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py 2014-03-13 20:59:10 +0000
+++ tests/functional/openlp_core_common/test_applocation.py 2014-03-18 21:03:35 +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-03-14 22:08:44 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py 2014-03-18 21:03:35 +0000
@@ -63,16 +63,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')
@@ -82,3 +83,37 @@
with self.assertRaises(KeyError) as context:
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')
+ #print(len(self.image_manager._cache))
+
+ # 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')
+
=== 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-03-18 21:03:35 +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-03-18 21:03:35 +0000
@@ -0,0 +1,175 @@
+# -*- 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
+from mock import MagicMock
+from openlp.plugins.presentations.lib.impresscontroller import \
+ ImpressController, ImpressDocument, TextType
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+class TestLibModule(TestCase):
+
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ self.file_name = os.path.join(TEST_PATH, '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):
+ 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):
+ 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
=== added file 'tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py 2014-03-18 21:03:35 +0000
@@ -0,0 +1,144 @@
+# -*- 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 PowerPointController class and related methods.
+"""
+from unittest import TestCase
+import os
+from mock import MagicMock
+from openlp.plugins.presentations.lib.powerpointcontroller import \
+ PowerpointController, PowerpointDocument, _get_text_from_shapes
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+
+class TestLibModule(TestCase):
+
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ self.ppc = PowerpointController(mocked_plugin)
+ self.file_name = os.path.join(TEST_PATH, "test.pptx")
+ self.doc = PowerpointDocument(self.ppc, self.file_name)
+
+ # add _test to the name to enable
+ def verify_installation(self):
+ """
+ Test the installation of Powerpoint
+ """
+ # GIVEN: A boolean value set to true
+ # WHEN: We "convert" it to a bool
+ is_installed = self.ppc.check_available()
+
+ # THEN: We should get back a True bool
+ self.assertEqual(is_installed, True, 'The result should be True')
+
+ # add _test to the following if necessary
+ def verify_loading_document(self):
+ """
+ Test loading a document in PowerPoint
+ """
+ # GIVEN: the filename
+ print(self.file_name)
+
+ # WHEN: loading the filename
+ self.doc = PowerpointDocument(self.ppc, self.file_name)
+ self.doc.load_presentation()
+ result = self.doc.is_loaded()
+
+ # THEN: result should be true
+ self.assertEqual(result, True, 'The result should be True')
+
+ def create_titles_and_notes_test(self):
+ """
+ Test creating the titles from PowerPoint
+ """
+ # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+ self.doc = PowerpointDocument(self.ppc, 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'], [' ', ' '])
+
+ def create_titles_and_notes_with_no_slides_test(self):
+ """
+ Test creating the titles from PowerPoint when it returns no slides
+ """
+ # GIVEN: mocked save_titles_and_notes, _get_text_from_shapes and two mocked slides
+ self.doc = PowerpointDocument(self.ppc, self.file_name)
+ self.doc.save_titles_and_notes = MagicMock()
+ self.doc._PowerpointDocument__get_text_from_shapes = MagicMock()
+ pres = MagicMock()
+ pres.Slides = []
+ 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 empty titles and notes
+ self.doc.save_titles_and_notes.assert_called_once_with([], [])
+
+ 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')
=== added file 'tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py'
--- tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/presentations/test_powerpointviewercontroller.py 2014-03-18 21:03:35 +0000
@@ -0,0 +1,151 @@
+# -*- 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 PptviewController class and related methods.
+"""
+from unittest import TestCase, SkipTest
+import os
+
+if os.name != 'nt':
+ raise SkipTest('Not Windows, skipping test')
+
+from mock import MagicMock, patch, mock_open
+from openlp.plugins.presentations.lib.pptviewcontroller import PptviewController, PptviewDocument
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
+
+class TestLibModule(TestCase):
+
+ def setUp(self):
+ mocked_plugin = MagicMock()
+ mocked_plugin.settings_section = 'presentations'
+ self.ppc = PptviewController(mocked_plugin)
+ self.file_name = os.path.join(TEST_PATH, "test.pptx")
+ self.doc = PptviewDocument(self.ppc, self.file_name)
+
+ #add _test to the function name to enable test
+ def verify_installation(self):
+ """
+ Test the installation of PowerpointViewer
+ """
+ # GIVEN: A boolean value set to true
+ # WHEN: We "convert" it to a bool
+ is_installed = self.ppc.check_available()
+ # THEN: We should get back a True bool
+ self.assertEqual(is_installed, True, 'The result should be True')
+
+ # add _test to the following if necessary to enable test
+ # I don't have powerpointviewer to verify
+ def verify_loading_document(self):
+ """
+ Test loading a document in PowerpointViewer
+ """
+ # GIVEN: the filename
+ print(self.file_name)
+ # WHEN: loading the filename
+ self.doc = PptviewDocument(self.ppc, self.file_name)
+ self.doc.load_presentation()
+ result = self.doc.is_loaded()
+ # THEN: result should be true
+ self.assertEqual(result, True, 'The result should be True')
+
+ # disabled
+ def verify_titles(self):
+ """
+ Test reading the titles from PowerpointViewer
+ """
+ # GIVEN:
+ self.doc = PptviewDocument(self.ppc, self.file_name)
+ self.doc.create_titles_and_notes()
+ # WHEN reading the titles and notes
+ titles, notes = self.doc.get_titles_and_notes()
+ print("titles: ".join(titles))
+ print("notes: ".join(notes))
+ # THEN there should be exactly 5 titles and 5 notes
+ self.assertEqual(len(titles), 5, 'There should be five titles')
+ self.assertEqual(len(notes), 5, 'There should be five notes')
+
+ def create_titles_and_notes_test(self):
+ """
+ Test PowerpointController.create_titles_and_notes
+ """
+ # GIVEN: mocked PresentationController.save_titles_and_notes
+ self.doc.save_titles_and_notes = MagicMock()
+
+ # WHEN reading the titles and notes
+ self.doc.create_titles_and_notes()
+
+ # THEN save_titles_and_notes should have been called once with empty arrays
+ self.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
+ self.doc = PptviewDocument(self.ppc, 'Idontexist.pptx')
+ self.doc.save_titles_and_notes = MagicMock()
+
+ # WHEN: reading the titles and notes
+ self.doc.create_titles_and_notes()
+
+ # THEN:
+ self.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', mock_open(read_data='this is a test')) as mocked_open, \
+ patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
+ patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
+ mocked_is_zf.return_value = False
+ mocked_exists.return_value = True
+ mocked_open.filesize = 10
+ self.doc = PptviewDocument(self.ppc, os.path.join(TEST_PATH, "test.ppt"))
+ self.doc.save_titles_and_notes = MagicMock()
+
+ # WHEN: reading the titles and notes
+ self.doc.create_titles_and_notes()
+
+ # THEN:
+ self.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_pptviewcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py 2014-03-08 21:23:47 +0000
+++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py 2014-03-18 21:03:35 +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, #
=== 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-03-18 21:03:35 +0000
@@ -1,158 +1,169 @@
-# -*- 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-03-14 22:08:44 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py 2014-03-18 21:03:35 +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
+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
@@ -111,7 +113,7 @@
Test the get_content_type logic
"""
# GIVEN: a set of files and their corresponding types
- headers = [ ['test.html', 'text/html'], ['test.css', 'text/css'],
+ headers = [['test.html', 'text/html'], ['test.css', 'text/css'],
['test.js', 'application/javascript'], ['test.jpg', 'image/jpeg'],
['test.gif', 'image/gif'], ['test.ico', 'image/x-icon'],
['test.png', 'image/png'], ['test.whatever', 'text/plain'],
@@ -157,7 +159,7 @@
self.router.html_dir = os.path.normpath('test/dir')
self.router.template_vars = MagicMock()
with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
- patch('builtins.open', mock_open(read_data='123')):
+ patch('builtins.open', mock_open(read_data='123')):
mocked_exists.return_value = True
# WHEN: call serve_file with an existing html file
@@ -167,3 +169,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/functional/openlp_plugins/remotes/test_websocket.py'
--- tests/functional/openlp_plugins/remotes/test_websocket.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_websocket.py 2014-03-18 21:03:35 +0000
@@ -0,0 +1,123 @@
+# -*- 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 WebSockets
+"""
+import base64
+import uuid
+import socket
+import time
+from unittest import TestCase
+
+from openlp.plugins.remotes.lib.websocket import WebSocketManager, ThreadedWebSocketHandler, \
+ WEB_SOCKET_CLIENT_HEADERS
+from tests.functional import MagicMock, patch, mock_open
+
+
+class TestWebSockets(TestCase):
+ """
+ Test the functions in the :mod:`lib` module.
+ """
+
+ def setUp(self):
+ """
+ Setup the WebSocketsManager
+ """
+ self.manager = WebSocketManager()
+ self.manager.start()
+
+ def tearDown(self):
+ self.manager.stop()
+
+ def attempt_to_talk_with_no_handshake_test(self):
+ """
+ Test the websocket without handshaking first
+ """
+ # GIVEN: A default configuration
+
+ # WHEN: attempts to talk without upgrading to websocket
+ # Create a socket (SOCK_STREAM means a TCP socket)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ data = bytes('No upgrade', 'utf-8')
+ received = None
+ try:
+ # Connect to server and send data
+ sock.connect(('localhost', 8888))
+ sock.send(data)
+ # Receive data from the server and shut down
+ received = sock.recv(1024)
+ finally:
+ sock.close()
+
+ # THEN:
+ self.assertIs(isinstance(self.manager, WebSocketManager), True,
+ 'It should be an object of WebSocketsManager type')
+ self.assertRegexpMatches(received.decode('utf-8'), '.*Error:.*', 'Mismatch')
+
+ def handshake_and_talk_test(self):
+ """
+ Test the websocket handshake
+ """
+ # GIVEN: A default configuration
+
+ # WHEN: upgrade to websocket and then talk
+ print("starting the websocket server")
+ print("started")
+ # Create a socket (SOCK_STREAM means a TCP socket)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Fake a handshake
+ uid = uuid.uuid4()
+ key = base64.encodebytes(uid.bytes).strip()
+ data = bytes('\r\n'.join(WEB_SOCKET_CLIENT_HEADERS).format(host='localhost', port='8888', key=key), 'utf-8')
+ received = None
+ try:
+ # Connect to server and send data
+ sock.connect(('localhost', 8888))
+ print("connected")
+ sock.send(data)
+ #print("data sent: ", data.decode('utf-8'))
+ # Receive data from the server and shut down
+ time.sleep(1)
+ received = sock.recv(1024)
+ print("data received: ", received.decode('utf-8'))
+ time.sleep(1)
+ self.manager.send('broadcast')
+ time.sleep(1)
+ received_broadcast = sock.recv(1024)
+ print(received_broadcast)
+ decoded_broadcast = ThreadedWebSocketHandler.decode_client_websocket_message(received_broadcast)
+ finally:
+ time.sleep(1)
+ sock.close()
+
+ # THEN:
+ self.assertIs(isinstance(self.manager, WebSocketManager), True,
+ 'It should be an object of WebSocketsManager type')
+ self.assertRegexpMatches(received.decode('utf-8'), '.*Upgrade: websocket.*', 'Handshake failed')
+ self.assertRegexpMatches(decoded_broadcast, '.*broadcast', 'WebSocket did not receive correct string')
=== added file 'tests/resources/test.pptx'
Binary files tests/resources/test.pptx 1970-01-01 00:00:00 +0000 and tests/resources/test.pptx 2014-03-18 21:03:35 +0000 differ
Follow ups