Merge trunk

This commit is contained in:
Samuel Mehrbrodt 2014-10-13 19:40:12 +02:00
commit 806bb5973e
54 changed files with 1529 additions and 409 deletions

View File

@ -36,10 +36,9 @@ if __name__ == '__main__':
""" """
Instantiate and run the application. Instantiate and run the application.
""" """
# Mac OS X passes arguments like '-psn_XXXX' to gui application. # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number.
# This argument is process serial number. However, this causes # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it
# conflict with other OpenLP arguments. Since we do not use this # to avoid any potential conflicts.
# argument we can delete it to avoid any potential conflicts.
if sys.platform.startswith('darwin'): if sys.platform.startswith('darwin'):
sys.argv = [x for x in sys.argv if not x.startswith('-psn')] sys.argv = [x for x in sys.argv if not x.startswith('-psn')]
main() main()

View File

@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main']
log = logging.getLogger() log = logging.getLogger()
NT_REPAIR_STYLESHEET = """ WIN_REPAIR_STYLESHEET = """
QMainWindow::separator QMainWindow::separator
{ {
border: none; border: none;
@ -127,7 +127,7 @@ class OpenLP(OpenLPMixin, QtGui.QApplication):
'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
application_stylesheet += alternate_rows_repair_stylesheet application_stylesheet += alternate_rows_repair_stylesheet
if is_win(): if is_win():
application_stylesheet += NT_REPAIR_STYLESHEET application_stylesheet += WIN_REPAIR_STYLESHEET
if application_stylesheet: if application_stylesheet:
self.setStyleSheet(application_stylesheet) self.setStyleSheet(application_stylesheet)
show_splash = Settings().value('core/show splash') show_splash = Settings().value('core/show splash')

View File

@ -33,6 +33,7 @@ import logging
import inspect import inspect
from openlp.core.common import trace_error_handler from openlp.core.common import trace_error_handler
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent'] 'preview_size_changed', 'resizeEvent']
@ -41,11 +42,8 @@ class OpenLPMixin(object):
""" """
Base Calling object for OpenLP classes. Base Calling object for OpenLP classes.
""" """
def __init__(self, parent): def __init__(self, *args, **kwargs):
try: super(OpenLPMixin, self).__init__(*args, **kwargs)
super(OpenLPMixin, self).__init__(parent)
except TypeError:
super(OpenLPMixin, self).__init__()
self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
if self.logger.getEffectiveLevel() == logging.DEBUG: if self.logger.getEffectiveLevel() == logging.DEBUG:
for name, m in inspect.getmembers(self, inspect.ismethod): for name, m in inspect.getmembers(self, inspect.ismethod):

View File

@ -145,11 +145,13 @@ def build_icon(icon):
return button_icon return button_icon
def image_to_byte(image): def image_to_byte(image, base_64=True):
""" """
Resize an image to fit on the current screen for the web and returns it as a byte stream. Resize an image to fit on the current screen for the web and returns it as a byte stream.
:param image: The image to converted. :param image: The image to converted.
:param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
To preserve original intention, this defaults to True
""" """
log.debug('image_to_byte - start') log.debug('image_to_byte - start')
byte_array = QtCore.QByteArray() byte_array = QtCore.QByteArray()
@ -158,6 +160,8 @@ def image_to_byte(image):
buffie.open(QtCore.QIODevice.WriteOnly) buffie.open(QtCore.QIODevice.WriteOnly)
image.save(buffie, "PNG") image.save(buffie, "PNG")
log.debug('image_to_byte - end') log.debug('image_to_byte - end')
if not base_64:
return byte_array
# convert to base64 encoding so does not get missed! # convert to base64 encoding so does not get missed!
return bytes(byte_array.toBase64()).decode('utf-8') return bytes(byte_array.toBase64()).decode('utf-8')

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import logging
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
from openlp.core.common import Registry, UiStrings, translate from openlp.core.common import Registry, UiStrings, translate, is_macosx
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.utils.actions import ActionList from openlp.core.utils.actions import ActionList
@ -247,6 +247,8 @@ def create_action(parent, name, **kwargs):
""" """
action = QtGui.QAction(parent) action = QtGui.QAction(parent)
action.setObjectName(name) action.setObjectName(name)
if is_macosx():
action.setIconVisibleInMenu(False)
if kwargs.get('text'): if kwargs.get('text'):
action.setText(kwargs.pop('text')) action.setText(kwargs.pop('text'))
if kwargs.get('icon'): if kwargs.get('icon'):

View File

@ -387,17 +387,21 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
self.progress_bar.setValue(self.progress_bar.maximum()) self.progress_bar.setValue(self.progress_bar.maximum())
if self.has_run_wizard: if self.has_run_wizard:
self.progress_label.setText(translate('OpenLP.FirstTimeWizard', self.progress_label.setText(translate('OpenLP.FirstTimeWizard',
'Download complete. Click the finish button to return to OpenLP.')) 'Download complete. Click the %s button to return to OpenLP.') %
self.buttonText(QtGui.QWizard.FinishButton))
else: else:
self.progress_label.setText(translate('OpenLP.FirstTimeWizard', self.progress_label.setText(translate('OpenLP.FirstTimeWizard',
'Download complete. Click the finish button to start OpenLP.')) 'Download complete. Click the %s button to start OpenLP.') %
self.buttonText(QtGui.QWizard.FinishButton))
else: else:
if self.has_run_wizard: if self.has_run_wizard:
self.progress_label.setText(translate('OpenLP.FirstTimeWizard', self.progress_label.setText(translate('OpenLP.FirstTimeWizard',
'Click the finish button to return to OpenLP.')) 'Click the %s button to return to OpenLP.') %
self.buttonText(QtGui.QWizard.FinishButton))
else: else:
self.progress_label.setText(translate('OpenLP.FirstTimeWizard', self.progress_label.setText(translate('OpenLP.FirstTimeWizard',
'Click the finish button to start OpenLP.')) 'Click the %s button to start OpenLP.') %
self.buttonText(QtGui.QWizard.FinishButton))
self.finish_button.setVisible(True) self.finish_button.setVisible(True)
self.finish_button.setEnabled(True) self.finish_button.setEnabled(True)
self.cancel_button.setVisible(False) self.cancel_button.setVisible(False)

View File

@ -31,9 +31,7 @@ The UI widgets for the first time wizard.
""" """
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
import sys from openlp.core.common import translate, is_macosx
from openlp.core.common import translate
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page from openlp.core.lib.ui import add_welcome_page
@ -64,9 +62,12 @@ class Ui_FirstTimeWizard(object):
first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
first_time_wizard.resize(550, 386) first_time_wizard.resize(550, 386)
first_time_wizard.setModal(True) first_time_wizard.setModal(True)
first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle)
first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage |
QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1) QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1)
if is_macosx():
first_time_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap,
QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
first_time_wizard.resize(634, 386)
self.finish_button = self.button(QtGui.QWizard.FinishButton) self.finish_button = self.button(QtGui.QWizard.FinishButton)
self.no_internet_finish_button = self.button(QtGui.QWizard.CustomButton1) self.no_internet_finish_button = self.button(QtGui.QWizard.CustomButton1)
self.cancel_button = self.button(QtGui.QWizard.CancelButton) self.cancel_button = self.button(QtGui.QWizard.CancelButton)
@ -212,7 +213,8 @@ class Ui_FirstTimeWizard(object):
translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')) translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard'))
self.information_label.setText( self.information_label.setText(
translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. '
'Click the next button below to start.')) 'Click the %s button below to start.') %
self.buttonText(QtGui.QWizard.NextButton))
self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins'))
self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. '))
self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs'))
@ -236,7 +238,7 @@ class Ui_FirstTimeWizard(object):
'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.')
self.cancelWizardText = translate('OpenLP.FirstTimeWizard', self.cancelWizardText = translate('OpenLP.FirstTimeWizard',
'\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), '
'click the Cancel button now.') 'click the %s button now.') % self.buttonText(QtGui.QWizard.CancelButton)
self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs')) self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs'))
self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.'))
self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles'))

View File

@ -60,6 +60,12 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont
""" """
super(FormattingTagForm, self).__init__(parent) super(FormattingTagForm, self).__init__(parent)
self.setupUi(self) self.setupUi(self)
self._setup()
def _setup(self):
"""
Set up the class. This method is mocked out by the tests.
"""
self.services = FormattingTagController() self.services = FormattingTagController()
self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected)
self.new_button.clicked.connect(self.on_new_clicked) self.new_button.clicked.connect(self.on_new_clicked)

View File

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

View File

@ -92,6 +92,8 @@ class Ui_MainWindow(object):
main_window.setObjectName('MainWindow') main_window.setObjectName('MainWindow')
main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg'))
main_window.setDockNestingEnabled(True) main_window.setDockNestingEnabled(True)
if is_macosx():
main_window.setDocumentMode(True)
# Set up the main container, which contains all the other form widgets. # Set up the main container, which contains all the other form widgets.
self.main_content = QtGui.QWidget(main_window) self.main_content = QtGui.QWidget(main_window)
self.main_content.setObjectName('main_content') self.main_content.setObjectName('main_content')
@ -118,10 +120,12 @@ class Ui_MainWindow(object):
self.recent_files_menu = QtGui.QMenu(self.file_menu) self.recent_files_menu = QtGui.QMenu(self.file_menu)
self.recent_files_menu.setObjectName('recentFilesMenu') self.recent_files_menu.setObjectName('recentFilesMenu')
self.file_import_menu = QtGui.QMenu(self.file_menu) self.file_import_menu = QtGui.QMenu(self.file_menu)
self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) if not is_macosx():
self.file_import_menu.setIcon(build_icon(u':/general/general_import.png'))
self.file_import_menu.setObjectName('file_import_menu') self.file_import_menu.setObjectName('file_import_menu')
self.file_export_menu = QtGui.QMenu(self.file_menu) self.file_export_menu = QtGui.QMenu(self.file_menu)
self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) if not is_macosx():
self.file_export_menu.setIcon(build_icon(u':/general/general_export.png'))
self.file_export_menu.setObjectName('file_export_menu') self.file_export_menu.setObjectName('file_export_menu')
# View Menu # View Menu
self.view_menu = QtGui.QMenu(self.menu_bar) self.view_menu = QtGui.QMenu(self.menu_bar)

View File

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

View File

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

View File

@ -31,7 +31,7 @@ The Create/Edit theme wizard
""" """
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate from openlp.core.common import UiStrings, translate, is_macosx
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
@ -41,19 +41,21 @@ class Ui_ThemeWizard(object):
""" """
The Create/Edit theme wizard The Create/Edit theme wizard
""" """
def setupUi(self, themeWizard): def setupUi(self, theme_wizard):
""" """
Set up the UI Set up the UI
""" """
themeWizard.setObjectName('OpenLP.ThemeWizard') theme_wizard.setObjectName('OpenLP.ThemeWizard')
themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
themeWizard.setModal(True) theme_wizard.setModal(True)
themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle) theme_wizard.setOptions(QtGui.QWizard.IndependentPages |
themeWizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1)
QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) if is_macosx():
theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
theme_wizard.resize(646, 400)
self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum)
# Welcome Page # Welcome Page
add_welcome_page(themeWizard, ':/wizards/wizard_createtheme.bmp') add_welcome_page(theme_wizard, ':/wizards/wizard_createtheme.bmp')
# Background Page # Background Page
self.background_page = QtGui.QWizardPage() self.background_page = QtGui.QWizardPage()
self.background_page.setObjectName('background_page') self.background_page.setObjectName('background_page')
@ -137,7 +139,7 @@ class Ui_ThemeWizard(object):
self.transparent_layout.setObjectName('Transparent_layout') self.transparent_layout.setObjectName('Transparent_layout')
self.background_stack.addWidget(self.transparent_widget) self.background_stack.addWidget(self.transparent_widget)
self.background_layout.addLayout(self.background_stack) self.background_layout.addLayout(self.background_stack)
themeWizard.addPage(self.background_page) theme_wizard.addPage(self.background_page)
# Main Area Page # Main Area Page
self.main_area_page = QtGui.QWizardPage() self.main_area_page = QtGui.QWizardPage()
self.main_area_page.setObjectName('main_area_page') self.main_area_page.setObjectName('main_area_page')
@ -218,7 +220,7 @@ class Ui_ThemeWizard(object):
self.shadow_size_spin_box.setObjectName('shadow_size_spin_box') self.shadow_size_spin_box.setObjectName('shadow_size_spin_box')
self.shadow_layout.addWidget(self.shadow_size_spin_box) self.shadow_layout.addWidget(self.shadow_size_spin_box)
self.main_area_layout.addRow(self.shadow_check_box, self.shadow_layout) self.main_area_layout.addRow(self.shadow_check_box, self.shadow_layout)
themeWizard.addPage(self.main_area_page) theme_wizard.addPage(self.main_area_page)
# Footer Area Page # Footer Area Page
self.footer_area_page = QtGui.QWizardPage() self.footer_area_page = QtGui.QWizardPage()
self.footer_area_page.setObjectName('footer_area_page') self.footer_area_page.setObjectName('footer_area_page')
@ -242,7 +244,7 @@ class Ui_ThemeWizard(object):
self.footer_size_spin_box.setObjectName('FooterSizeSpinBox') self.footer_size_spin_box.setObjectName('FooterSizeSpinBox')
self.footer_area_layout.addRow(self.footer_size_label, self.footer_size_spin_box) self.footer_area_layout.addRow(self.footer_size_label, self.footer_size_spin_box)
self.footer_area_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) self.footer_area_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer)
themeWizard.addPage(self.footer_area_page) theme_wizard.addPage(self.footer_area_page)
# Alignment Page # Alignment Page
self.alignment_page = QtGui.QWizardPage() self.alignment_page = QtGui.QWizardPage()
self.alignment_page.setObjectName('alignment_page') self.alignment_page.setObjectName('alignment_page')
@ -264,7 +266,7 @@ class Ui_ThemeWizard(object):
self.transitions_check_box.setObjectName('transitions_check_box') self.transitions_check_box.setObjectName('transitions_check_box')
self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box) self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box)
self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer)
themeWizard.addPage(self.alignment_page) theme_wizard.addPage(self.alignment_page)
# Area Position Page # Area Position Page
self.area_position_page = QtGui.QWizardPage() self.area_position_page = QtGui.QWizardPage()
self.area_position_page.setObjectName('area_position_page') self.area_position_page.setObjectName('area_position_page')
@ -334,7 +336,7 @@ class Ui_ThemeWizard(object):
self.footer_height_spin_box.setObjectName('footer_height_spin_box') self.footer_height_spin_box.setObjectName('footer_height_spin_box')
self.footer_position_layout.addRow(self.footer_height_label, self.footer_height_spin_box) self.footer_position_layout.addRow(self.footer_height_label, self.footer_height_spin_box)
self.area_position_layout.addWidget(self.footer_position_group_box) self.area_position_layout.addWidget(self.footer_position_group_box)
themeWizard.addPage(self.area_position_page) theme_wizard.addPage(self.area_position_page)
# Preview Page # Preview Page
self.preview_page = QtGui.QWizardPage() self.preview_page = QtGui.QWizardPage()
self.preview_page.setObjectName('preview_page') self.preview_page.setObjectName('preview_page')
@ -362,8 +364,8 @@ class Ui_ThemeWizard(object):
self.preview_box_label.setObjectName('preview_box_label') self.preview_box_label.setObjectName('preview_box_label')
self.preview_area_layout.addWidget(self.preview_box_label) self.preview_area_layout.addWidget(self.preview_box_label)
self.preview_layout.addWidget(self.preview_area) self.preview_layout.addWidget(self.preview_area)
themeWizard.addPage(self.preview_page) theme_wizard.addPage(self.preview_page)
self.retranslateUi(themeWizard) self.retranslateUi(theme_wizard)
QtCore.QObject.connect(self.background_combo_box, QtCore.SIGNAL('currentIndexChanged(int)'), QtCore.QObject.connect(self.background_combo_box, QtCore.SIGNAL('currentIndexChanged(int)'),
self.background_stack, QtCore.SLOT('setCurrentIndex(int)')) self.background_stack, QtCore.SLOT('setCurrentIndex(int)'))
QtCore.QObject.connect(self.outline_check_box, QtCore.SIGNAL('toggled(bool)'), self.outline_color_button, QtCore.QObject.connect(self.outline_check_box, QtCore.SIGNAL('toggled(bool)'), self.outline_color_button,
@ -391,11 +393,11 @@ class Ui_ThemeWizard(object):
QtCore.QObject.connect(self.footer_position_check_box, QtCore.SIGNAL('toggled(bool)'), QtCore.QObject.connect(self.footer_position_check_box, QtCore.SIGNAL('toggled(bool)'),
self.footer_height_spin_box, QtCore.SLOT('setDisabled(bool)')) self.footer_height_spin_box, QtCore.SLOT('setDisabled(bool)'))
def retranslateUi(self, themeWizard): def retranslateUi(self, theme_wizard):
""" """
Translate the UI on the fly Translate the UI on the fly
""" """
themeWizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) theme_wizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard'))
self.title_label.setText('<span style="font-size:14pt; font-weight:600;">%s</span>' % self.title_label.setText('<span style="font-size:14pt; font-weight:600;">%s</span>' %
translate('OpenLP.ThemeWizard', 'Welcome to the Theme Wizard')) translate('OpenLP.ThemeWizard', 'Welcome to the Theme Wizard'))
self.information_label.setText( self.information_label.setText(
@ -484,8 +486,8 @@ class Ui_ThemeWizard(object):
self.footer_height_label.setText(translate('OpenLP.ThemeWizard', 'Height:')) self.footer_height_label.setText(translate('OpenLP.ThemeWizard', 'Height:'))
self.footer_height_spin_box.setSuffix(translate('OpenLP.ThemeWizard', 'px')) self.footer_height_spin_box.setSuffix(translate('OpenLP.ThemeWizard', 'px'))
self.footer_position_check_box.setText(translate('OpenLP.ThemeWizard', 'Use default location')) self.footer_position_check_box.setText(translate('OpenLP.ThemeWizard', 'Use default location'))
themeWizard.setOption(QtGui.QWizard.HaveCustomButton1, False) theme_wizard.setOption(QtGui.QWizard.HaveCustomButton1, False)
themeWizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) theme_wizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview'))
self.preview_page.setTitle(translate('OpenLP.ThemeWizard', 'Preview and Save')) self.preview_page.setTitle(translate('OpenLP.ThemeWizard', 'Preview and Save'))
self.preview_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Preview the theme and save it.')) self.preview_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Preview the theme and save it.'))
self.theme_name_label.setText(translate('OpenLP.ThemeWizard', 'Theme name:')) self.theme_name_label.setText(translate('OpenLP.ThemeWizard', 'Theme name:'))

View File

@ -34,7 +34,7 @@ import os
from PyQt4 import QtGui from PyQt4 import QtGui
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx
from openlp.core.lib import build_icon from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page from openlp.core.lib.ui import add_welcome_page
@ -121,9 +121,10 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
""" """
self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.setModal(True) self.setModal(True)
self.setWizardStyle(QtGui.QWizard.ModernStyle)
self.setOptions(QtGui.QWizard.IndependentPages | self.setOptions(QtGui.QWizard.IndependentPages |
QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage)
if is_macosx():
self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
add_welcome_page(self, image) add_welcome_page(self, image)
self.add_custom_pages() self.add_custom_pages()
if self.with_progress_page: if self.with_progress_page:

View File

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

View File

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

View File

@ -28,31 +28,29 @@
############################################################################### ###############################################################################
import os import os
if os.name == 'nt':
from win32com.client import Dispatch
import string
import sys
if sys.platform.startswith('linux'):
import dbus
import logging import logging
import re import re
from time import sleep from time import sleep
from datetime import datetime from datetime import datetime
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
from openlp.core.common import translate from openlp.core.common import translate, is_win, is_linux, is_macosx, RegistryProperties
from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.media import format_milliseconds
if is_win():
from win32com.client import Dispatch
if is_linux():
import dbus
try: try:
from openlp.core.ui.media.vendor import vlc from openlp.core.ui.media.vendor import vlc
except (ImportError, NameError, NotImplementedError): except (ImportError, NameError, NotImplementedError):
pass pass
except OSError as e: except OSError as e:
if sys.platform.startswith('win'): if is_win():
if not isinstance(e, WindowsError) and e.winerror != 126: if not isinstance(e, WindowsError) and e.winerror != 126:
raise raise
else: else:
@ -61,7 +59,7 @@ except OSError as e:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryProperties):
""" """
Class to manage the clip selection Class to manage the clip selection
""" """
@ -144,9 +142,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
# You have to give the id of the QFrame (or similar object) # You have to give the id of the QFrame (or similar object)
# to vlc, different platforms have different functions for this. # to vlc, different platforms have different functions for this.
win_id = int(self.preview_frame.winId()) win_id = int(self.preview_frame.winId())
if sys.platform == "win32": if is_win():
self.vlc_media_player.set_hwnd(win_id) self.vlc_media_player.set_hwnd(win_id)
elif sys.platform == "darwin": elif is_macosx():
# We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa
# framework and not the old Carbon. # framework and not the old Carbon.
self.vlc_media_player.set_nsobject(win_id) self.vlc_media_player.set_nsobject(win_id)
@ -190,7 +188,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
self.audio_cd = True self.audio_cd = True
self.titles_combo_box.setDisabled(False) self.titles_combo_box.setDisabled(False)
self.titles_combo_box.setCurrentIndex(0) self.titles_combo_box.setCurrentIndex(0)
self.on_title_combo_box_currentIndexChanged(0) self.on_titles_combo_box_currentIndexChanged(0)
return True return True
@ -203,18 +201,21 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
""" """
log.debug('on_load_disc_button_clicked') log.debug('on_load_disc_button_clicked')
self.disable_all() self.disable_all()
self.application.set_busy_cursor()
path = self.media_path_combobox.currentText() path = self.media_path_combobox.currentText()
# Check if given path is non-empty and exists before starting VLC # Check if given path is non-empty and exists before starting VLC
if not path: if not path:
log.debug('no given path') log.debug('no given path')
critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given'))
self.toggle_disable_load_media(False) self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
return return
if not os.path.exists(path): if not os.path.exists(path):
log.debug('Given path does not exists') log.debug('Given path does not exists')
critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm',
'Given path does not exists')) 'Given path does not exists'))
self.toggle_disable_load_media(False) self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
return return
# VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to
# detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS.
@ -231,6 +232,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm',
'An error happened during initialization of VLC player')) 'An error happened during initialization of VLC player'))
self.toggle_disable_load_media(False) self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
return return
# put the media in the media player # put the media in the media player
self.vlc_media_player.set_media(self.vlc_media) self.vlc_media_player.set_media(self.vlc_media)
@ -241,6 +243,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm',
'VLC player failed playing the media')) 'VLC player failed playing the media'))
self.toggle_disable_load_media(False) self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
self.vlc_media_player.audio_set_mute(False)
return return
self.vlc_media_player.audio_set_mute(True) self.vlc_media_player.audio_set_mute(True)
if not self.media_state_wait(vlc.State.Playing): if not self.media_state_wait(vlc.State.Playing):
@ -249,23 +253,32 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm',
'VLC player failed playing the media')) 'VLC player failed playing the media'))
self.toggle_disable_load_media(False) self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
self.vlc_media_player.audio_set_mute(False)
return return
self.vlc_media_player.audio_set_mute(True) # pause
self.vlc_media_player.set_time(0)
self.vlc_media_player.set_pause(1)
self.media_state_wait(vlc.State.Paused)
self.toggle_disable_load_media(False)
self.application.set_normal_cursor()
self.vlc_media_player.audio_set_mute(False)
if not self.audio_cd: if not self.audio_cd:
# Temporarily disable signals
self.blockSignals(True)
# Get titles, insert in combobox # Get titles, insert in combobox
titles = self.vlc_media_player.video_get_title_description() titles = self.vlc_media_player.video_get_title_description()
self.titles_combo_box.clear() self.titles_combo_box.clear()
for title in titles: for title in titles:
self.titles_combo_box.addItem(title[1].decode(), title[0]) self.titles_combo_box.addItem(title[1].decode(), title[0])
# Re-enable signals
self.blockSignals(False)
# Main title is usually title #1 # Main title is usually title #1
if len(titles) > 1: if len(titles) > 1:
self.titles_combo_box.setCurrentIndex(1) self.titles_combo_box.setCurrentIndex(1)
else:
self.titles_combo_box.setCurrentIndex(0)
# Enable audio track combobox if anything is in it # Enable audio track combobox if anything is in it
if len(titles) > 0: if len(titles) > 0:
self.titles_combo_box.setDisabled(False) self.titles_combo_box.setDisabled(False)
self.toggle_disable_load_media(False)
log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state())
@QtCore.pyqtSlot(bool) @QtCore.pyqtSlot(bool)
@ -378,6 +391,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
if not self.vlc_media_player: if not self.vlc_media_player:
log.error('vlc_media_player was None') log.error('vlc_media_player was None')
return return
self.application.set_busy_cursor()
if self.audio_cd: if self.audio_cd:
self.vlc_media = self.audio_cd_tracks.item_at_index(index) self.vlc_media = self.audio_cd_tracks.item_at_index(index)
self.vlc_media_player.set_media(self.vlc_media) self.vlc_media_player.set_media(self.vlc_media)
@ -385,14 +399,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
self.vlc_media_player.play() self.vlc_media_player.play()
if not self.media_state_wait(vlc.State.Playing): if not self.media_state_wait(vlc.State.Playing):
log.error('Could not start playing audio cd, needed to get track info') log.error('Could not start playing audio cd, needed to get track info')
self.application.set_normal_cursor()
return return
self.vlc_media_player.audio_set_mute(True) self.vlc_media_player.audio_set_mute(True)
# Sleep 1 second to make sure VLC has the needed metadata
sleep(1)
# pause # pause
self.vlc_media_player.set_time(0) self.vlc_media_player.set_time(0)
self.vlc_media_player.set_pause(1) self.vlc_media_player.set_pause(1)
self.vlc_media_player.audio_set_mute(False) self.vlc_media_player.audio_set_mute(False)
self.application.set_normal_cursor()
self.toggle_disable_player(False) self.toggle_disable_player(False)
else: else:
self.vlc_media_player.set_title(index) self.vlc_media_player.set_title(index)
@ -400,13 +414,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
self.vlc_media_player.play() self.vlc_media_player.play()
if not self.media_state_wait(vlc.State.Playing): if not self.media_state_wait(vlc.State.Playing):
log.error('Could not start playing dvd, needed to get track info') log.error('Could not start playing dvd, needed to get track info')
self.application.set_normal_cursor()
return return
self.vlc_media_player.audio_set_mute(True) self.vlc_media_player.audio_set_mute(True)
# Sleep 1 second to make sure VLC has the needed metadata # Get audio tracks
sleep(1)
self.vlc_media_player.set_time(0)
# Get audio tracks, insert in combobox
audio_tracks = self.vlc_media_player.audio_get_track_description() audio_tracks = self.vlc_media_player.audio_get_track_description()
log.debug('number of audio tracks: %d' % len(audio_tracks))
# Clear the audio track combobox, insert new tracks
self.audio_tracks_combobox.clear() self.audio_tracks_combobox.clear()
for audio_track in audio_tracks: for audio_track in audio_tracks:
self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0])
@ -447,6 +461,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
self.vlc_media_player.set_pause(1) self.vlc_media_player.set_pause(1)
loop_count += 1 loop_count += 1
log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state())
self.application.set_normal_cursor()
@QtCore.pyqtSlot(int) @QtCore.pyqtSlot(int)
def on_audio_tracks_combobox_currentIndexChanged(self, index): def on_audio_tracks_combobox_currentIndexChanged(self, index):
@ -535,7 +550,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
""" """
Saves the current media and trackinfo as a clip to the mediamanager Saves the current media and trackinfo as a clip to the mediamanager
""" """
log.debug('in on_save_button_clicked') log.debug('in MediaClipSelectorForm.accept')
start_time = self.start_position_edit.time() start_time = self.start_position_edit.time()
start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time_ms = start_time.hour() * 60 * 60 * 1000 + \
start_time.minute() * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \
@ -550,10 +565,23 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
path = self.media_path_combobox.currentText() path = self.media_path_combobox.currentText()
optical = '' optical = ''
if self.audio_cd: if self.audio_cd:
# Check for load problems
if start_time_ms is None or end_time_ms is None or title is None:
critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'CD not loaded correctly'),
translate('MediaPlugin.MediaClipSelectorForm',
'The CD was not loaded correctly, please re-load and try again.'))
return
optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms)
else: else:
audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex())
subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex())
# Check for load problems
if start_time_ms is None or end_time_ms is None or title is None or audio_track is None\
or subtitle_track is None:
critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'DVD not loaded correctly'),
translate('MediaPlugin.MediaClipSelectorForm',
'The DVD was not loaded correctly, please re-load and try again.'))
return
optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms)
# Ask for an alternative name for the mediaclip # Ask for an alternative name for the mediaclip
while True: while True:
@ -595,7 +623,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
while media_state != self.vlc_media_player.get_state(): while media_state != self.vlc_media_player.get_state():
if self.vlc_media_player.get_state() == vlc.State.Error: if self.vlc_media_player.get_state() == vlc.State.Error:
return False return False
if (datetime.now() - start).seconds > 30: if (datetime.now() - start).seconds > 15:
return False return False
return True return True
@ -606,7 +634,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
""" """
# Clear list first # Clear list first
self.media_path_combobox.clear() self.media_path_combobox.clear()
if os.name == 'nt': if is_win():
# use win api to find optical drives # use win api to find optical drives
fso = Dispatch('scripting.filesystemobject') fso = Dispatch('scripting.filesystemobject')
for drive in fso.Drives: for drive in fso.Drives:
@ -614,7 +642,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
# if type is 4, it is a cd-rom drive # if type is 4, it is a cd-rom drive
if drive.DriveType == 4: if drive.DriveType == 4:
self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter)
elif sys.platform.startswith('linux'): elif is_linux():
# Get disc devices from dbus and find the ones that are optical # Get disc devices from dbus and find the ones that are optical
bus = dbus.SystemBus() bus = dbus.SystemBus()
try: try:
@ -646,7 +674,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector):
if chr(c) != '\x00': if chr(c) != '\x00':
block_file += chr(c) block_file += chr(c)
self.media_path_combobox.addItem(block_file) self.media_path_combobox.addItem(block_file)
elif sys.platform.startswith('darwin'): elif is_macosx():
# Look for DVD folders in devices to find optical devices # Look for DVD folders in devices to find optical devices
volumes = os.listdir('/Volumes') volumes = os.listdir('/Volumes')
candidates = list() candidates = list()

View File

@ -65,7 +65,7 @@ from PyQt4 import QtCore
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
from .presentationcontroller import PresentationController, PresentationDocument from .presentationcontroller import PresentationController, PresentationDocument, TextType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -257,6 +257,7 @@ class ImpressDocument(PresentationDocument):
self.presentation.Display = ScreenList().current['number'] + 1 self.presentation.Display = ScreenList().current['number'] + 1
self.control = None self.control = None
self.create_thumbnails() self.create_thumbnails()
self.create_titles_and_notes()
return True return True
def create_thumbnails(self): def create_thumbnails(self):
@ -450,22 +451,44 @@ class ImpressDocument(PresentationDocument):
:param slide_no: The slide the notes are required for, starting at 1 :param slide_no: The slide the notes are required for, starting at 1
""" """
return self.__get_text_from_page(slide_no, True) return self.__get_text_from_page(slide_no, TextType.Notes)
def __get_text_from_page(self, slide_no, notes=False): def __get_text_from_page(self, slide_no, text_type=TextType.SlideText):
""" """
Return any text extracted from the presentation page. Return any text extracted from the presentation page.
:param slide_no: The slide the notes are required for, starting at 1 :param slide_no: The slide the notes are required for, starting at 1
:param notes: A boolean. If set the method searches the notes of the slide. :param notes: A boolean. If set the method searches the notes of the slide.
:param text_type: A TextType. Enumeration of the types of supported text.
""" """
text = '' text = ''
pages = self.document.getDrawPages() if TextType.Title <= text_type <= TextType.Notes:
page = pages.getByIndex(slide_no - 1) pages = self.document.getDrawPages()
if notes: if 0 < slide_no <= pages.getCount():
page = page.getNotesPage() page = pages.getByIndex(slide_no - 1)
for index in range(page.getCount()): if text_type == TextType.Notes:
shape = page.getByIndex(index) page = page.getNotesPage()
if shape.supportsService("com.sun.star.drawing.Text"): for index in range(page.getCount()):
text += shape.getString() + '\n' shape = page.getByIndex(index)
shape_type = shape.getShapeType()
if shape.supportsService("com.sun.star.drawing.Text"):
# if they requested title, make sure it is the title
if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape":
text += shape.getString() + '\n'
return text return text
def create_titles_and_notes(self):
"""
Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt'
in the thumbnails directory
"""
titles = []
notes = []
pages = self.document.getDrawPages()
for slide_no in range(1, pages.getCount() + 1):
titles.append(self.__get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n')
note = self.__get_text_from_page(slide_no, TextType.Notes)
if len(note) == 0:
note = ' '
notes.append(note)
self.save_titles_and_notes(titles, notes)

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -293,6 +293,49 @@ class PresentationDocument(object):
""" """
return '' return ''
def get_titles_and_notes(self):
"""
Reads the titles from the titles file and
the notes files and returns the content in two lists
"""
titles = []
notes = []
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
if os.path.exists(titles_file):
try:
with open(titles_file) as fi:
titles = fi.read().splitlines()
except:
log.exception('Failed to open/read existing titles file')
titles = []
for slide_no, title in enumerate(titles, 1):
notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
note = ''
if os.path.exists(notes_file):
try:
with open(notes_file) as fn:
note = fn.read()
except:
log.exception('Failed to open/read notes file')
note = ''
notes.append(note)
return titles, notes
def save_titles_and_notes(self, titles, notes):
"""
Performs the actual persisting of titles to the titles.txt
and notes to the slideNote%.txt
"""
if titles:
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
with open(titles_file, mode='w') as fo:
fo.writelines(titles)
if notes:
for slide_no, note in enumerate(notes, 1):
notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no)
with open(notes_file, mode='w') as fn:
fn.write(note)
class PresentationController(object): class PresentationController(object):
""" """
@ -427,3 +470,12 @@ class PresentationController(object):
def close_presentation(self): def close_presentation(self):
pass pass
class TextType(object):
"""
Type Enumeration for Types of Text to request
"""
Title = 0
SlideText = 1
Notes = 2

View File

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

View File

@ -102,7 +102,21 @@ window.OpenLP = {
$("#verseorder span").removeClass("currenttag"); $("#verseorder span").removeClass("currenttag");
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag"); $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
var slide = OpenLP.currentSlides[OpenLP.currentSlide]; var slide = OpenLP.currentSlides[OpenLP.currentSlide];
var text = slide["text"]; var text = "";
// use title if available
if (slide["title"]) {
text = slide["title"];
} else {
text = slide["text"];
}
// use thumbnail if available
if (slide["img"]) {
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
}
// use notes if available
if (slide["notes"]) {
text += '<br />' + slide["notes"];
}
text = text.replace(/\n/g, "<br />"); text = text.replace(/\n/g, "<br />");
$("#currentslide").html(text); $("#currentslide").html(text);
text = ""; text = "";
@ -110,7 +124,11 @@ window.OpenLP = {
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) { for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "<p class=\"nextslide\">"; text = text + "<p class=\"nextslide\">";
text = text + OpenLP.currentSlides[idx]["text"]; if (OpenLP.currentSlides[idx]["title"]) {
text = text + OpenLP.currentSlides[idx]["title"];
} else {
text = text + OpenLP.currentSlides[idx]["text"];
}
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
text = text + "</p>"; text = text + "</p>";
else else

View File

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

View File

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

View File

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

View File

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

View File

@ -31,8 +31,6 @@ The :mod:`db` module provides the database and schema that is the backend for
the Songs plugin the Songs plugin
""" """
import re
from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.sql.expression import func, text from sqlalchemy.sql.expression import func, text
@ -329,7 +327,9 @@ def init_schema(url):
Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True) Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True)
) )
mapper(Author, authors_table) mapper(Author, authors_table, properties={
'songs': relation(Song, secondary=authors_songs_table, viewonly=True)
})
mapper(AuthorSong, authors_songs_table, properties={ mapper(AuthorSong, authors_songs_table, properties={
'author': relation(Author) 'author': relation(Author)
}) })
@ -339,7 +339,8 @@ def init_schema(url):
# Use the authors_songs relation when you need access to the 'author_type' attribute # Use the authors_songs relation when you need access to the 'author_type' attribute
# or when creating new relations # or when creating new relations
'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"), 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"),
'authors': relation(Author, secondary=authors_songs_table, viewonly=True), # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198)
'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'),
'book': relation(Book, backref='songs'), 'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table) 'topics': relation(Topic, backref='songs', secondary=songs_topics_table)

View File

@ -99,6 +99,7 @@
<file>export_load.png</file> <file>export_load.png</file>
</qresource> </qresource>
<qresource prefix="wizards"> <qresource prefix="wizards">
<file>openlp-osx-wizard.png</file>
<file>wizard_exportsong.bmp</file> <file>wizard_exportsong.bmp</file>
<file>wizard_importsong.bmp</file> <file>wizard_importsong.bmp</file>
<file>wizard_importbible.bmp</file> <file>wizard_importbible.bmp</file>
@ -151,10 +152,10 @@
<file>messagebox_warning.png</file> <file>messagebox_warning.png</file>
</qresource> </qresource>
<qresource prefix="remote"> <qresource prefix="remote">
<file>network_server.png</file> <file>network_server.png</file>
<file>network_ssl.png</file> <file>network_ssl.png</file>
<file>network_auth.png</file> <file>network_auth.png</file>
</qresource> </qresource>
<qresource prefix="songusage"> <qresource prefix="songusage">
<file>song_usage_active.png</file> <file>song_usage_active.png</file>
<file>song_usage_inactive.png</file> <file>song_usage_inactive.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -162,9 +162,9 @@ class TestAppLocation(TestCase):
patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \ patch('openlp.core.common.applocation.os.path.abspath') as mocked_abspath, \
patch('openlp.core.common.applocation.os.path.split') as mocked_split, \ patch('openlp.core.common.applocation.os.path.split') as mocked_split, \
patch('openlp.core.common.applocation.sys') as mocked_sys: patch('openlp.core.common.applocation.sys') as mocked_sys:
mocked_abspath.return_value = 'plugins/dir' mocked_abspath.return_value = os.path.join('plugins', 'dir')
mocked_split.return_value = ['openlp'] mocked_split.return_value = ['openlp']
mocked_get_frozen_path.return_value = 'plugins/dir' mocked_get_frozen_path.return_value = os.path.join('plugins', 'dir')
mocked_sys.frozen = 1 mocked_sys.frozen = 1
mocked_sys.argv = ['openlp'] mocked_sys.argv = ['openlp']
@ -172,7 +172,7 @@ class TestAppLocation(TestCase):
directory = AppLocation.get_directory(AppLocation.PluginsDir) directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN: The correct directory should be returned # THEN: The correct directory should be returned
self.assertEqual('plugins/dir', directory, 'Directory should be "plugins/dir"') self.assertEqual(os.path.join('plugins', 'dir'), directory, 'Directory should be "plugins/dir"')
def get_frozen_path_in_unfrozen_app_test(self): def get_frozen_path_in_unfrozen_app_test(self):
""" """

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class TestTheme(TestCase):
""" """
pass pass
def test_new_theme(self): def new_theme_test(self):
""" """
Test the theme creation - basic test Test the theme creation - basic test
""" """

View File

@ -29,10 +29,14 @@
""" """
Package to test the openlp.core.lib.ui package. Package to test the openlp.core.lib.ui package.
""" """
from PyQt4 import QtGui from PyQt4 import QtCore, QtGui
from unittest import TestCase from unittest import TestCase
from openlp.core.lib.ui import * from openlp.core.common import UiStrings, translate
from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \
create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \
set_case_insensitive_completer
from tests.functional import MagicMock, patch
class TestUi(TestCase): class TestUi(TestCase):
@ -40,7 +44,7 @@ class TestUi(TestCase):
Test the functions in the ui module Test the functions in the ui module
""" """
def test_add_welcome_page(self): def add_welcome_page_test(self):
""" """
Test appending a welcome page to a wizard Test appending a welcome page to a wizard
""" """
@ -54,7 +58,7 @@ class TestUi(TestCase):
self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.')
self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap) self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap)
def test_create_button_box(self): def create_button_box_test(self):
""" """
Test creating a button box for a dialog Test creating a button box for a dialog
""" """
@ -82,7 +86,7 @@ class TestUi(TestCase):
self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(1, len(btnbox.buttons()))
self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0]))
def test_create_horizontal_adjusting_combo_box(self): def create_horizontal_adjusting_combo_box_test(self):
""" """
Test creating a horizontal adjusting combo box Test creating a horizontal adjusting combo box
""" """
@ -97,7 +101,7 @@ class TestUi(TestCase):
self.assertEqual('combo1', combo.objectName()) self.assertEqual('combo1', combo.objectName())
self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy())
def test_create_button(self): def create_button_test(self):
""" """
Test creating a button Test creating a button
""" """
@ -129,7 +133,7 @@ class TestUi(TestCase):
self.assertEqual('my_btn', btn.objectName()) self.assertEqual('my_btn', btn.objectName())
self.assertTrue(btn.isEnabled()) self.assertTrue(btn.isEnabled())
def test_create_action(self): def create_action_test(self):
""" """
Test creating an action Test creating an action
""" """
@ -154,9 +158,46 @@ class TestUi(TestCase):
self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my tooltip', action.toolTip())
self.assertEqual('my statustip', action.statusTip()) self.assertEqual('my statustip', action.statusTip())
def test_create_checked_enabled_visible_action(self): def create_action_on_mac_osx_test(self):
""" """
Test creating an action with the 'checked', 'enabled' and 'visible' properties. Test creating an action on OS X calls the correct method
"""
# GIVEN: A dialog and a mocked out is_macosx() method to always return True
with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \
patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction:
mocked_is_macosx.return_value = True
mocked_action = MagicMock()
MockedQAction.return_value = mocked_action
dialog = QtGui.QDialog()
# WHEN: An action is created
create_action(dialog, 'my_action')
# THEN: setIconVisibleInMenu should be called
mocked_action.setIconVisibleInMenu.assert_called_with(False)
def create_action_not_on_mac_osx_test(self):
"""
Test creating an action on something other than OS X doesn't call the method
"""
# GIVEN: A dialog and a mocked out is_macosx() method to always return True
with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \
patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction:
mocked_is_macosx.return_value = False
mocked_action = MagicMock()
MockedQAction.return_value = mocked_action
dialog = QtGui.QDialog()
# WHEN: An action is created
create_action(dialog, 'my_action')
# THEN: setIconVisibleInMenu should not be called
self.assertEqual(0, mocked_action.setIconVisibleInMenu.call_count,
'setIconVisibleInMenu should not have been called')
def create_checked_disabled_invisible_action_test(self):
"""
Test that an invisible, disabled, checked action is created correctly
""" """
# GIVEN: A dialog # GIVEN: A dialog
dialog = QtGui.QDialog() dialog = QtGui.QDialog()
@ -165,11 +206,24 @@ class TestUi(TestCase):
action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False)
# THEN: These properties should be set # THEN: These properties should be set
self.assertEqual(True, action.isChecked()) self.assertTrue(action.isChecked(), 'The action should be checked')
self.assertEqual(False, action.isEnabled()) self.assertFalse(action.isEnabled(), 'The action should be disabled')
self.assertEqual(False, action.isVisible()) self.assertFalse(action.isVisible(), 'The action should be invisble')
def test_create_valign_selection_widgets(self): def create_action_separator_test(self):
"""
Test creating an action as separator
"""
# GIVEN: A dialog
dialog = QtGui.QDialog()
# WHEN: We create an action as a separator
action = create_action(dialog, 'my_action', separator=True)
# THEN: The action should be a separator
self.assertTrue(action.isSeparator(), 'The action should be a separator')
def create_valign_selection_widgets_test(self):
""" """
Test creating a combo box for valign selection Test creating a combo box for valign selection
""" """
@ -186,7 +240,7 @@ class TestUi(TestCase):
for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
self.assertTrue(combo.findText(text) >= 0) self.assertTrue(combo.findText(text) >= 0)
def test_find_and_set_in_combo_box(self): def find_and_set_in_combo_box_test(self):
""" """
Test finding a string in a combo box and setting it as the selected item if present Test finding a string in a combo box and setting it as the selected item if present
""" """
@ -213,7 +267,7 @@ class TestUi(TestCase):
# THEN: The index should have changed # THEN: The index should have changed
self.assertEqual(2, combo.currentIndex()) self.assertEqual(2, combo.currentIndex())
def test_create_widget_action(self): def create_widget_action_test(self):
""" """
Test creating an action for a widget Test creating an action for a widget
""" """
@ -227,7 +281,7 @@ class TestUi(TestCase):
self.assertIsInstance(action, QtGui.QAction) self.assertIsInstance(action, QtGui.QAction)
self.assertEqual(action.objectName(), 'some action') self.assertEqual(action.objectName(), 'some action')
def test_set_case_insensitive_completer(self): def set_case_insensitive_completer_test(self):
""" """
Test setting a case insensitive completer on a widget Test setting a case insensitive completer on a widget
""" """

View File

@ -47,7 +47,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
Registry().register('application', self.app) Registry().register('application', self.app)
self.first_time_form = FirstTimeForm(screens) self.first_time_form = FirstTimeForm(screens)
def test_access_to_config(self): def access_to_config_test(self):
""" """
Test if we can access the First Time Form's config file Test if we can access the First Time Form's config file
""" """
@ -59,7 +59,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
self.assertTrue(self.first_time_form.web_access, self.assertTrue(self.first_time_form.web_access,
'First Time Wizard\'s web configuration file should be available') 'First Time Wizard\'s web configuration file should be available')
def test_parsable_config(self): def parsable_config_test(self):
""" """
Test if the First Time Form's config file is parsable Test if the First Time Form's config file is parsable
""" """

View File

@ -39,7 +39,7 @@ class TestFormattingTagController(TestCase):
def setUp(self): def setUp(self):
self.services = FormattingTagController() self.services = FormattingTagController()
def test_strip(self): def strip_test(self):
""" """
Test that the _strip strips the correct chars Test that the _strip strips the correct chars
""" """
@ -52,7 +52,7 @@ class TestFormattingTagController(TestCase):
# THEN: The tag should be returned with the wrappers removed. # THEN: The tag should be returned with the wrappers removed.
self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'') self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'')
def test_end_tag_changed_processes_correctly(self): def end_tag_changed_processes_correctly_test(self):
""" """
Test that the end html tags are generated correctly Test that the end html tags are generated correctly
""" """
@ -77,7 +77,7 @@ class TestFormattingTagController(TestCase):
self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' %
error) error)
def test_start_tag_changed_processes_correctly(self): def start_tag_changed_processes_correctly_test(self):
""" """
Test that the end html tags are generated correctly Test that the end html tags are generated correctly
""" """
@ -100,7 +100,7 @@ class TestFormattingTagController(TestCase):
self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' %
error) error)
def test_start_html_to_end_html(self): def start_html_to_end_html_test(self):
""" """
Test that the end html tags are generated correctly Test that the end html tags are generated correctly
""" """

View File

@ -29,17 +29,17 @@
""" """
Package to test the openlp.core.ui.formattingtagsform package. Package to test the openlp.core.ui.formattingtagsform package.
""" """
from PyQt4 import QtGui
from unittest import TestCase from unittest import TestCase
from openlp.core.common import translate
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch, call
from openlp.core.ui.formattingtagform import FormattingTagForm from openlp.core.ui.formattingtagform import FormattingTagForm
# TODO: Tests Still TODO # TODO: Tests Still TODO
# __init__ # __init__
# exec_ # exec_
# on_new_clicked
# on_delete_clicked
# on_saved_clicked # on_saved_clicked
# _reloadTable # _reloadTable
@ -47,30 +47,60 @@ from openlp.core.ui.formattingtagform import FormattingTagForm
class TestFormattingTagForm(TestCase): class TestFormattingTagForm(TestCase):
def setUp(self): def setUp(self):
self.init_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm.__init__') """
self.qdialog_patcher = patch('openlp.core.ui.formattingtagform.QtGui.QDialog') Mock out stuff for all the tests
self.ui_formatting_tag_dialog_patcher = patch('openlp.core.ui.formattingtagform.Ui_FormattingTagDialog') """
self.mocked_init = self.init_patcher.start() self.setup_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm._setup')
self.mocked_qdialog = self.qdialog_patcher.start() self.setup_patcher.start()
self.mocked_ui_formatting_tag_dialog = self.ui_formatting_tag_dialog_patcher.start()
self.mocked_init.return_value = None
def tearDown(self): def tearDown(self):
self.init_patcher.stop()
self.qdialog_patcher.stop()
self.ui_formatting_tag_dialog_patcher.stop()
def test_on_text_edited(self):
""" """
Test that the appropriate actions are preformed when on_text_edited is called Remove the mocks
"""
self.setup_patcher.stop()
def on_row_selected_test(self):
"""
Test that the appropriate actions are preformed when on_row_selected is called
"""
# GIVEN: An instance of the Formatting Tag Form and a mocked delete_button
form = FormattingTagForm(None)
form.delete_button = MagicMock()
# WHEN: on_row_selected is called
form.on_row_selected()
# THEN: setEnabled and should have been called on delete_button
form.delete_button.setEnabled.assert_called_with(True)
def on_new_clicked_test(self):
"""
Test that clicking the Add a new tag button does the right thing
""" """
# GIVEN: An instance of the Formatting Tag Form and a mocked save_push_button # GIVEN: A formatting tag form and a mocked out tag table widget
form = FormattingTagForm() form = FormattingTagForm(None)
form.save_button = MagicMock() form.tag_table_widget = MagicMock()
row_count = 5
form.tag_table_widget.rowCount.return_value = row_count
# WHEN: on_text_edited is called with an arbitrary value # WHEN: on_new_clicked is run (i.e. the Add new button was clicked)
# form.on_text_edited('text') with patch('openlp.core.ui.formattingtagform.QtGui.QTableWidgetItem') as MockedQTableWidgetItem:
mocked_table_widget = MagicMock()
MockedQTableWidgetItem.return_value = mocked_table_widget
form.on_new_clicked()
# THEN: setEnabled and setDefault should have been called on save_push_button # THEN: A new row should be added to the table
# form.save_button.setEnabled.assert_called_with(True) form.tag_table_widget.rowCount.assert_called_with()
form.tag_table_widget.insertRow.assert_called_with(row_count)
expected_set_item_calls = [
call(row_count, 0, mocked_table_widget),
call(row_count, 1, mocked_table_widget),
call(row_count, 2, mocked_table_widget),
call(row_count, 3, mocked_table_widget)
]
self.assertEqual(expected_set_item_calls, form.tag_table_widget.setItem.call_args_list,
'setItem should have been called correctly')
form.tag_table_widget.resizeRowsToContents.assert_called_with()
form.tag_table_widget.scrollToBottom.assert_called_with()
form.tag_table_widget.selectRow.assert_called_with(row_count)

View File

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

View File

@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Functional tests to test the Impress class and related methods.
"""
from unittest import TestCase
import os
import shutil
from tempfile import mkdtemp
from tests.functional import patch, MagicMock
from tests.utils.constants import TEST_RESOURCES_PATH
from tests.helpers.testmixin import TestMixin
from openlp.plugins.presentations.lib.impresscontroller import \
ImpressController, ImpressDocument, TextType
class TestImpressController(TestCase, TestMixin):
"""
Test the ImpressController Class
"""
def setUp(self):
"""
Set up the patches and mocks need for all tests.
"""
self.get_application()
self.build_settings()
self.mock_plugin = MagicMock()
self.temp_folder = mkdtemp()
self.mock_plugin.settings_section = self.temp_folder
def tearDown(self):
"""
Stop the patches
"""
self.destroy_settings()
shutil.rmtree(self.temp_folder)
def constructor_test(self):
"""
Test the Constructor from the ImpressController
"""
# GIVEN: No presentation controller
controller = None
# WHEN: The presentation controller object is created
controller = ImpressController(plugin=self.mock_plugin)
# THEN: The name of the presentation controller should be correct
self.assertEqual('Impress', controller.name,
'The name of the presentation controller should be correct')
class TestImpressDocument(TestCase):
"""
Test the ImpressDocument Class
"""
def setUp(self):
mocked_plugin = MagicMock()
mocked_plugin.settings_section = 'presentations'
self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
self.ppc = ImpressController(mocked_plugin)
self.doc = ImpressDocument(self.ppc, self.file_name)
def create_titles_and_notes_test(self):
"""
Test ImpressDocument.create_titles_and_notes
"""
# GIVEN: mocked PresentationController.save_titles_and_notes with
# 0 pages and the LibreOffice Document
self.doc.save_titles_and_notes = MagicMock()
self.doc.document = MagicMock()
self.doc.document.getDrawPages.return_value = MagicMock()
self.doc.document.getDrawPages().getCount.return_value = 0
# WHEN reading the titles and notes
self.doc.create_titles_and_notes()
# THEN save_titles_and_notes should have been called with empty arrays
self.doc.save_titles_and_notes.assert_called_once_with([], [])
# GIVEN: reset mock and set it to 2 pages
self.doc.save_titles_and_notes.reset_mock()
self.doc.document.getDrawPages().getCount.return_value = 2
# WHEN: a new call to create_titles_and_notes
self.doc.create_titles_and_notes()
# THEN: save_titles_and_notes should have been called once with
# two arrays of two elements
self.doc.save_titles_and_notes.assert_called_once_with(['\n', '\n'], [' ', ' '])
def get_text_from_page_out_of_bound_test(self):
"""
Test ImpressDocument.__get_text_from_page with out-of-bounds index
"""
# GIVEN: mocked LibreOffice Document with one slide,
# two notes and three texts
self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
# WHEN: __get_text_from_page is called with an index of 0x00
result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Notes)
# THEN: the result should be an empty string
self.assertEqual(result, '', 'Result should be an empty string')
# WHEN: regardless of the type of text, index 0x00 is out of bounds
result = self.doc._ImpressDocument__get_text_from_page(0, TextType.Title)
# THEN: result should be an empty string
self.assertEqual(result, '', 'Result should be an empty string')
# WHEN: when called with 2, it should also be out of bounds
result = self.doc._ImpressDocument__get_text_from_page(2, TextType.SlideText)
# THEN: result should be an empty string ... and, getByIndex should
# have never been called
self.assertEqual(result, '', 'Result should be an empty string')
self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
'There should be no call to getByIndex')
def get_text_from_page_wrong_type_test(self):
"""
Test ImpressDocument.__get_text_from_page with wrong TextType
"""
# GIVEN: mocked LibreOffice Document with one slide, two notes and
# three texts
self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
# WHEN: called with TextType 3
result = self.doc._ImpressDocument__get_text_from_page(1, 3)
# THEN: result should be an empty string
self.assertEqual(result, '', 'Result should be and empty string')
self.assertEqual(self.doc.document.getDrawPages().getByIndex.call_count, 0,
'There should be no call to getByIndex')
def get_text_from_page_valid_params_test(self):
"""
Test ImpressDocument.__get_text_from_page with valid parameters
"""
# GIVEN: mocked LibreOffice Document with one slide,
# two notes and three texts
self.doc.document = self._mock_a_LibreOffice_document(1, 2, 3)
# WHEN: __get_text_from_page is called to get the Notes
result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Notes)
# THEN: result should be 'Note\nNote\n'
self.assertEqual(result, 'Note\nNote\n', 'Result should be \'Note\\n\' times the count of notes in the page')
# WHEN: get the Title
result = self.doc._ImpressDocument__get_text_from_page(1, TextType.Title)
# THEN: result should be 'Title\n'
self.assertEqual(result, 'Title\n', 'Result should be exactly \'Title\\n\'')
# WHEN: get all text
result = self.doc._ImpressDocument__get_text_from_page(1, TextType.SlideText)
# THEN: result should be 'Title\nString\nString\n'
self.assertEqual(result, 'Title\nString\nString\n', 'Result should be exactly \'Title\\nString\\nString\\n\'')
def _mock_a_LibreOffice_document(self, page_count, note_count, text_count):
"""
Helper function, creates a mock libreoffice document.
:param page_count: Number of pages in the document
:param note_count: Number of note pages in the document
:param text_count: Number of text pages in the document
"""
pages = MagicMock()
page = MagicMock()
pages.getByIndex.return_value = page
notes_page = MagicMock()
notes_page.getCount.return_value = note_count
shape = MagicMock()
shape.supportsService.return_value = True
shape.getString.return_value = 'Note'
notes_page.getByIndex.return_value = shape
page.getNotesPage.return_value = notes_page
page.getCount.return_value = text_count
page.getByIndex.side_effect = self._get_page_shape_side_effect
pages.getCount.return_value = page_count
document = MagicMock()
document.getDrawPages.return_value = pages
document.getByIndex.return_value = page
return document
def _get_page_shape_side_effect(*args):
"""
Helper function.
"""
page_shape = MagicMock()
page_shape.supportsService.return_value = True
if args[1] == 0:
page_shape.getShapeType.return_value = 'com.sun.star.presentation.TitleTextShape'
page_shape.getString.return_value = 'Title'
else:
page_shape.getString.return_value = 'String'
return page_shape

View File

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

View File

@ -4,8 +4,8 @@
############################################################################### ###############################################################################
# OpenLP - Open Source Lyrics Projection # # OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman # # Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # # Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # # Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # # Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # # Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
@ -31,16 +31,19 @@ This module contains tests for the pptviewcontroller module of the Presentations
""" """
import os import os
import shutil import shutil
if os.name == 'nt':
from ctypes import cdll
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
from tests.utils.constants import TEST_RESOURCES_PATH
from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
from openlp.core.common import is_win
if is_win():
from ctypes import cdll
class TestPptviewController(TestCase, TestMixin): class TestPptviewController(TestCase, TestMixin):
@ -98,7 +101,7 @@ class TestPptviewController(TestCase, TestMixin):
available = controller.check_available() available = controller.check_available()
# THEN: On windows it should return True, on other platforms False # THEN: On windows it should return True, on other platforms False
if os.name == 'nt': if is_win():
self.assertTrue(available, 'check_available should return True on windows.') self.assertTrue(available, 'check_available should return True on windows.')
else: else:
self.assertFalse(available, 'check_available should return False when not on windows.') self.assertFalse(available, 'check_available should return False when not on windows.')
@ -130,7 +133,7 @@ class TestPptviewDocument(TestCase):
""" """
Set up the patches and mocks need for all tests. Set up the patches and mocks need for all tests.
""" """
self.os_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os') self.os_isdir_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.isdir')
self.pptview_document_create_thumbnails_patcher = patch( self.pptview_document_create_thumbnails_patcher = patch(
'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails') 'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails')
self.pptview_document_stop_presentation_patcher = patch( self.pptview_document_stop_presentation_patcher = patch(
@ -141,46 +144,45 @@ class TestPptviewDocument(TestCase):
'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup') 'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup')
self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList') self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList')
self.rect_patcher = MagicMock() self.rect_patcher = MagicMock()
self.mock_os_isdir = self.os_isdir_patcher.start()
self.mock_os = self.os_patcher.start()
self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start() self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start()
self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start() self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start()
self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start() self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start()
self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start() self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start()
self.mock_rect = self.rect_patcher.start() self.mock_rect = self.rect_patcher.start()
self.mock_screen_list = self.screen_list_patcher.start() self.mock_screen_list = self.screen_list_patcher.start()
self.mock_controller = MagicMock() self.mock_controller = MagicMock()
self.mock_presentation = MagicMock() self.mock_presentation = MagicMock()
self.temp_folder = mkdtemp()
self.mock_presentation_document_get_temp_folder.return_value = 'temp folder' self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder
def tearDown(self): def tearDown(self):
""" """
Stop the patches Stop the patches
""" """
self.os_patcher.stop() self.os_isdir_patcher.stop()
self.pptview_document_create_thumbnails_patcher.stop() self.pptview_document_create_thumbnails_patcher.stop()
self.pptview_document_stop_presentation_patcher.stop() self.pptview_document_stop_presentation_patcher.stop()
self.presentation_document_get_temp_folder_patcher.stop() self.presentation_document_get_temp_folder_patcher.stop()
self.presentation_document_setup_patcher.stop() self.presentation_document_setup_patcher.stop()
self.rect_patcher.stop() self.rect_patcher.stop()
self.screen_list_patcher.stop() self.screen_list_patcher.stop()
shutil.rmtree(self.temp_folder)
def load_presentation_succesfull_test(self): def load_presentation_succesfull_test(self):
""" """
Test the PptviewDocument.load_presentation() method when the PPT is successfully opened Test the PptviewDocument.load_presentation() method when the PPT is successfully opened
""" """
# GIVEN: A reset mocked_os # GIVEN: A reset mocked_os
self.mock_os.reset() self.mock_os_isdir.reset()
# WHEN: The temporary directory exists and OpenPPT returns successfully (not -1) # WHEN: The temporary directory exists and OpenPPT returns successfully (not -1)
self.mock_os.path.isdir.return_value = True self.mock_os_isdir.return_value = True
self.mock_controller.process.OpenPPT.return_value = 0 self.mock_controller.process.OpenPPT.return_value = 0
instance = PptviewDocument(self.mock_controller, self.mock_presentation) instance = PptviewDocument(self.mock_controller, self.mock_presentation)
instance.file_path = 'test\path.ppt' instance.file_path = 'test\path.ppt'
if os.name == 'nt': if is_win():
result = instance.load_presentation() result = instance.load_presentation()
# THEN: PptviewDocument.load_presentation should return True # THEN: PptviewDocument.load_presentation should return True
@ -191,17 +193,78 @@ class TestPptviewDocument(TestCase):
Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is
not successfully opened not successfully opened
""" """
# GIVEN: A reset mocked_os # GIVEN: A reset mock_os_isdir
self.mock_os.reset() self.mock_os_isdir.reset()
# WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1) # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1)
self.mock_os.path.isdir.return_value = False with patch('openlp.plugins.presentations.lib.pptviewcontroller.os.makedirs') as mock_makedirs:
self.mock_controller.process.OpenPPT.return_value = -1 self.mock_os_isdir.return_value = False
instance = PptviewDocument(self.mock_controller, self.mock_presentation) self.mock_controller.process.OpenPPT.return_value = -1
instance.file_path = 'test\path.ppt' instance = PptviewDocument(self.mock_controller, self.mock_presentation)
if os.name == 'nt': instance.file_path = 'test\path.ppt'
result = instance.load_presentation() if is_win():
result = instance.load_presentation()
# THEN: The temporary directory should be created and PptviewDocument.load_presentation should return False # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False
self.mock_os.makedirs.assert_called_once_with('temp folder') mock_makedirs.assert_called_once_with(self.temp_folder)
self.assertFalse(result) self.assertFalse(result)
def create_titles_and_notes_test(self):
"""
Test PowerpointController.create_titles_and_notes
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
doc.save_titles_and_notes = MagicMock()
# WHEN reading the titles and notes
doc.create_titles_and_notes()
# THEN save_titles_and_notes should have been called once with empty arrays
doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'],
['Notes for slide 1', 'Inserted', 'Notes for slide 2',
'Notes \nfor slide 4', 'Notes for slide 3'])
def create_titles_and_notes_nonexistent_file_test(self):
"""
Test PowerpointController.create_titles_and_notes with nonexistent file
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
with patch('builtins.open') as mocked_open, \
patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
mocked_dir_exists:
mocked_exists.return_value = False
mocked_dir_exists.return_value = False
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = 'Idontexist.pptx'
doc.save_titles_and_notes = MagicMock()
# WHEN: Reading the titles and notes
doc.create_titles_and_notes()
# THEN: File existens should have been checked, and not have been opened.
doc.save_titles_and_notes.assert_called_once_with(None, None)
mocked_exists.assert_any_call('Idontexist.pptx')
self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.')
def create_titles_and_notes_invalid_file_test(self):
"""
Test PowerpointController.create_titles_and_notes with invalid file
"""
# GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file
with patch('builtins.open') as mocked_open, \
patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf:
mocked_is_zf.return_value = False
mocked_open.filesize = 10
doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
doc.save_titles_and_notes = MagicMock()
# WHEN: reading the titles and notes
doc.create_titles_and_notes()
# THEN:
doc.save_titles_and_notes.assert_called_once_with(None, None)
self.assertEqual(mocked_is_zf.call_count, 1, 'is_zipfile should have been called once')

View File

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

View File

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

View File

@ -30,10 +30,12 @@
This module contains tests for the lib submodule of the Remotes plugin. This module contains tests for the lib submodule of the Remotes plugin.
""" """
import os import os
import urllib.request
from unittest import TestCase from unittest import TestCase
from openlp.core.common import Settings, Registry from openlp.core.common import Settings, Registry
from openlp.plugins.remotes.lib.httpserver import HttpRouter from openlp.plugins.remotes.lib.httpserver import HttpRouter
from urllib.parse import urlparse
from tests.functional import MagicMock, patch, mock_open from tests.functional import MagicMock, patch, mock_open
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -186,3 +188,86 @@ class TestRouter(TestCase, TestMixin):
self.router.send_response.assert_called_once_with(200) self.router.send_response.assert_called_once_with(200)
self.router.send_header.assert_called_once_with('Content-type', 'text/html') self.router.send_header.assert_called_once_with('Content-type', 'text/html')
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once') self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
def serve_thumbnail_without_params_test(self):
"""
Test the serve_thumbnail routine without params
"""
self.router.send_response = MagicMock()
self.router.send_header = MagicMock()
self.router.end_headers = MagicMock()
self.router.wfile = MagicMock()
self.router.serve_thumbnail()
self.router.send_response.assert_called_once_with(404)
self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
def serve_thumbnail_with_invalid_params_test(self):
"""
Test the serve_thumbnail routine with invalid params
"""
# GIVEN: Mocked send_header, send_response, end_headers and wfile
self.router.send_response = MagicMock()
self.router.send_header = MagicMock()
self.router.end_headers = MagicMock()
self.router.wfile = MagicMock()
# WHEN: pass a bad controller
self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png')
# THEN: a 404 should be returned
self.assertEqual(len(self.router.send_header.mock_calls), 1, 'One header')
self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response')
self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket')
self.router.send_response.assert_called_once_with(404)
# WHEN: pass a bad filename
self.router.send_response.reset_mock()
self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png')
# THEN: return a 404
self.router.send_response.assert_called_once_with(404)
# WHEN: a dangerous URL is passed
self.router.send_response.reset_mock()
self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png')
# THEN: return a 404
self.router.send_response.assert_called_once_with(404)
def serve_thumbnail_with_valid_params_test(self):
"""
Test the serve_thumbnail routine with valid params
"""
# GIVEN: Mocked send_header, send_response, end_headers and wfile
self.router.send_response = MagicMock()
self.router.send_header = MagicMock()
self.router.end_headers = MagicMock()
self.router.wfile = MagicMock()
mocked_image_manager = MagicMock()
Registry.create()
Registry().register('image_manager', mocked_image_manager)
file_name = 'another%20test/slide1.png'
full_path = os.path.normpath(os.path.join('thumbnails', file_name))
width = 120
height = 90
with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
patch('builtins.open', mock_open(read_data='123')), \
patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
mocked_exists.return_value = True
mocked_image_to_byte.return_value = '123'
mocked_location.get_section_data_path.return_value = ''
# WHEN: pass good controller and filename
result = self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name)
# THEN: a file should be returned
self.assertEqual(self.router.send_header.call_count, 1, 'One header')
self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
mocked_exists.assert_called_with(urllib.parse.unquote(full_path))
self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once')
mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'),
'slide1.png', None, '120x90')
mocked_image_manager.assert_called_any(os.path.normpath('thumbnails\\another test'), 'slide1.png', '120x90')

View File

@ -0,0 +1,28 @@
# -*- 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 #
###############################################################################

View File

@ -0,0 +1,28 @@
# -*- 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 #
###############################################################################

View File

@ -60,6 +60,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin):
# Mock VLC so we don't actually use it # Mock VLC so we don't actually use it
self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc')
self.vlc_patcher.start() self.vlc_patcher.start()
Registry().register('application', self.app)
# Mock the media item # Mock the media item
self.mock_media_item = MagicMock() self.mock_media_item = MagicMock()
# create form to test # create form to test
@ -67,6 +68,9 @@ class TestMediaClipSelectorForm(TestCase, TestMixin):
mock_media_state_wait = MagicMock() mock_media_state_wait = MagicMock()
mock_media_state_wait.return_value = True mock_media_state_wait.return_value = True
self.form.media_state_wait = mock_media_state_wait self.form.media_state_wait = mock_media_state_wait
self.form.application.set_busy_cursor = MagicMock()
self.form.application.set_normal_cursor = MagicMock()
self.form.find_optical_devices = MagicMock()
def tearDown(self): def tearDown(self):
""" """
@ -155,3 +159,21 @@ class TestMediaClipSelectorForm(TestCase, TestMixin):
self.form.audio_tracks_combobox.itemData.assert_any_call(0) self.form.audio_tracks_combobox.itemData.assert_any_call(0)
self.form.audio_tracks_combobox.itemData.assert_any_call(1) self.form.audio_tracks_combobox.itemData.assert_any_call(1)
self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) self.form.subtitle_tracks_combobox.itemData.assert_any_call(0)
def click_save_button_test(self):
"""
Test that the correct function is called when save is clicked, and that it behaves as expected.
"""
# GIVEN: Mocked methods.
with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \
mocked_critical_error_message_box,\
patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec:
self.form.exec_()
# WHEN: The save button is clicked with a NoneType in start_time_ms or end_time_ms
self.form.accept()
# THEN: we should get an error message
mocked_critical_error_message_box.assert_called_with('DVD not loaded correctly',
'The DVD was not loaded correctly, '
'please re-load and try again.')

Binary file not shown.

Binary file not shown.