This commit is contained in:
Andreas Preikschat 2013-04-20 22:30:51 +02:00
commit 3f60c34440
80 changed files with 1780 additions and 941 deletions

View File

@ -111,10 +111,10 @@ class OpenLP(QtGui.QApplication):
# Decide how many screens we have and their size
screens = ScreenList.create(self.desktop())
# First time checks in settings
has_run_wizard = Settings().value(u'general/has run wizard')
has_run_wizard = Settings().value(u'core/has run wizard')
if not has_run_wizard:
if FirstTimeForm(screens).exec_() == QtGui.QDialog.Accepted:
Settings().setValue(u'general/has run wizard', True)
Settings().setValue(u'core/has run wizard', True)
# Correct stylesheet bugs
application_stylesheet = u''
if not Settings().value(u'advanced/alternate rows'):
@ -126,7 +126,7 @@ class OpenLP(QtGui.QApplication):
application_stylesheet += NT_REPAIR_STYLESHEET
if application_stylesheet:
self.setStyleSheet(application_stylesheet)
show_splash = Settings().value(u'general/show splash')
show_splash = Settings().value(u'core/show splash')
if show_splash:
self.splash = SplashScreen()
self.splash.show()
@ -147,7 +147,7 @@ class OpenLP(QtGui.QApplication):
self.processEvents()
if not has_run_wizard:
self.main_window.first_time()
update_check = Settings().value(u'general/update check')
update_check = Settings().value(u'core/update check')
if update_check:
VersionThread(self.main_window).start()
self.main_window.is_display_blank()
@ -305,8 +305,10 @@ def main(args=None):
# Instance check
if application.is_already_running():
sys.exit()
# Remove/convert obsolete settings.
Settings().remove_obsolete_settings()
# First time checks in settings
if not Settings().value(u'general/has run wizard'):
if not Settings().value(u'core/has run wizard'):
if not FirstTimeLanguageForm().exec_():
# if cancel then stop processing
sys.exit()

View File

@ -103,6 +103,9 @@ class MediaManagerItem(QtGui.QWidget):
self.retranslateUi()
self.auto_select_id = -1
Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load)
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
def required_icons(self):
"""
@ -481,6 +484,15 @@ class MediaManagerItem(QtGui.QWidget):
else:
self.go_live()
def go_live_remote(self, message):
"""
Remote Call wrapper
``message``
The passed data item_id:Remote.
"""
self.go_live(message[0], remote=message[1])
def go_live(self, item_id=None, remote=False):
"""
Make the currently selected item go live.
@ -523,6 +535,15 @@ class MediaManagerItem(QtGui.QWidget):
for item in items:
self.add_to_service(item)
def add_to_service_remote(self, message):
"""
Remote Call wrapper
``message``
The passed data item:Remote.
"""
self.add_to_service(message[0], remote=message[1])
def add_to_service(self, item=None, replace=None, remote=False):
"""
Add this item to the current service.

View File

@ -103,7 +103,7 @@ class Plugin(QtCore.QObject):
``add_export_menu_Item(export_menu)``
Add an item to the Export menu.
``create_settings_Tab()``
``create_settings_tab()``
Creates a new instance of SettingsTabItem to be used in the Settings
dialog.
@ -252,7 +252,7 @@ class Plugin(QtCore.QObject):
"""
pass
def create_settings_Tab(self, parent):
def create_settings_tab(self, parent):
"""
Create a tab for the settings window to display the configurable options
for this plugin to the user.

View File

@ -153,7 +153,7 @@ class PluginManager(object):
"""
for plugin in self.plugins:
if plugin.status is not PluginStatus.Disabled:
plugin.create_settings_Tab(self.settings_form)
plugin.create_settings_tab(self.settings_form)
def hook_import_menu(self):
"""

View File

@ -247,15 +247,15 @@ class ScreenList(object):
# Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
# Do not do this anywhere else.
screen_settings = {
u'general/x position': self.current[u'size'].x(),
u'general/y position': self.current[u'size'].y(),
u'general/monitor': self.display_count - 1,
u'general/height': self.current[u'size'].height(),
u'general/width': self.current[u'size'].width()
u'core/x position': self.current[u'size'].x(),
u'core/y position': self.current[u'size'].y(),
u'core/monitor': self.display_count - 1,
u'core/height': self.current[u'size'].height(),
u'core/width': self.current[u'size'].width()
}
Settings.extend_default_settings(screen_settings)
settings = Settings()
settings.beginGroup(u'general')
settings.beginGroup(u'core')
monitor = settings.value(u'monitor')
self.set_current_display(monitor)
self.display = settings.value(u'display on monitor')

View File

@ -62,12 +62,10 @@ class ItemCapabilities(object):
tab when making the previous item live.
``CanEdit``
The capability to allow the ServiceManager to allow the item to be
edited
The capability to allow the ServiceManager to allow the item to be edited
``CanMaintain``
The capability to allow the ServiceManager to allow the item to be
reordered.
The capability to allow the ServiceManager to allow the item to be reordered.
``RequiresMedia``
Determines is the service_item needs a Media Player

View File

@ -116,30 +116,29 @@ class Settings(QtCore.QSettings):
u'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
u'crashreport/last directory': u'',
u'displayTags/html_tags': u'',
u'general/audio repeat list': False,
u'general/auto open': False,
u'general/auto preview': False,
u'general/audio start paused': True,
u'general/auto unblank': False,
u'general/blank warning': False,
u'general/ccli number': u'',
u'general/has run wizard': False,
u'general/language': u'[en]',
# This defaults to yesterday in order to force the update check to run when you've never run it before.
u'general/last version test': datetime.datetime.now().date() - datetime.timedelta(days=1),
u'general/loop delay': 5,
u'general/recent files': [],
u'general/save prompt': False,
u'general/screen blank': False,
u'general/show splash': True,
u'general/songselect password': u'',
u'general/songselect username': u'',
u'general/update check': True,
u'general/view mode': u'default',
u'core/audio repeat list': False,
u'core/auto open': False,
u'core/auto preview': False,
u'core/audio start paused': True,
u'core/auto unblank': False,
u'core/blank warning': False,
u'core/ccli number': u'',
u'core/has run wizard': False,
u'core/language': u'[en]',
u'core/last version test': u'',
u'core/loop delay': 5,
u'core/recent files': [],
u'core/save prompt': False,
u'core/screen blank': False,
u'core/show splash': True,
u'core/songselect password': u'',
u'core/songselect username': u'',
u'core/update check': True,
u'core/view mode': u'default',
# The other display settings (display position and dimensions) are defined in the ScreenList class due to a
# circular dependency.
u'general/display on monitor': True,
u'general/override position': False,
u'core/display on monitor': True,
u'core/override position': False,
u'images/background color': u'#000000',
u'media/players': u'webkit',
u'media/override player': QtCore.Qt.Unchecked,
@ -304,7 +303,7 @@ class Settings(QtCore.QSettings):
# Changed during 1.9.x development.
(u'bibles/bookname language', u'bibles/book name language', []),
(u'general/enable slide loop', u'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]),
(u'songs/ccli number', u'general/ccli number', []),
(u'songs/ccli number', u'core/ccli number', []),
(u'media/use phonon', u'', []),
# Changed during 2.1.x development.
(u'advanced/stylesheet fix', u'', []),
@ -315,7 +314,34 @@ class Settings(QtCore.QSettings):
(u'songs/last directory 1', u'songs/last directory import', []),
(u'songusage/last directory 1', u'songusage/last directory export', []),
(u'user interface/mainwindow splitter geometry', u'user interface/main window splitter geometry', []),
(u'shortcuts/makeLive', u'shortcuts/make_live', [])
(u'shortcuts/makeLive', u'shortcuts/make_live', []),
(u'general/audio repeat list', u'core/audio repeat list', []),
(u'general/auto open', u'core/auto open', []),
(u'general/auto preview', u'core/auto preview', []),
(u'general/audio start paused', u'core/audio start paused', []),
(u'general/auto unblank', u'core/auto unblank', []),
(u'general/blank warning', u'core/blank warning', []),
(u'general/ccli number', u'core/ccli number', []),
(u'general/has run wizard', u'core/has run wizard', []),
(u'general/language', u'core/language', []),
(u'general/last version test', u'core/last version test', []),
(u'general/loop delay', u'core/loop delay', []),
(u'general/recent files', u'core/recent files', []),
(u'general/save prompt', u'core/save prompt', []),
(u'general/screen blank', u'core/screen blank', []),
(u'general/show splash', u'core/show splash', []),
(u'general/songselect password', u'core/songselect password', []),
(u'general/songselect username', u'core/songselect username', []),
(u'general/update check', u'core/update check', []),
(u'general/view mode', u'core/view mode', []),
(u'general/display on monitor', u'core/display on monitor', []),
(u'general/override position', u'core/override position', []),
(u'general/x position', u'core/x position', []),
(u'general/y position', u'core/y position', []),
(u'general/monitor', u'core/monitor', []),
(u'general/height', u'core/height', []),
(u'general/monitor', u'core/monitor', []),
(u'general/width', u'core/width', [])
]
@staticmethod

View File

@ -69,6 +69,19 @@ try:
MAKO_VERSION = mako.__version__
except ImportError:
MAKO_VERSION = u'-'
try:
import icu
try:
ICU_VERSION = icu.VERSION
except AttributeError:
ICU_VERSION = u'OK'
except ImportError:
ICU_VERSION = u'-'
try:
import cherrypy
CHERRYPY_VERSION = cherrypy.__version__
except ImportError:
CHERRYPY_VERSION = u'-'
try:
import uno
arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue')
@ -143,6 +156,8 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog):
u'PyEnchant: %s\n' % ENCHANT_VERSION + \
u'PySQLite: %s\n' % SQLITE_VERSION + \
u'Mako: %s\n' % MAKO_VERSION + \
u'CherryPy: %s\n' % CHERRYPY_VERSION + \
u'pyICU: %s\n' % ICU_VERSION + \
u'pyUNO bridge: %s\n' % UNO_VERSION + \
u'VLC: %s\n' % VLC_VERSION
if platform.system() == u'Linux':

View File

@ -118,7 +118,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
check_directory_exists(os.path.join(unicode(gettempdir(), get_filesystem_encoding()), u'openlp'))
self.noInternetFinishButton.setVisible(False)
# Check if this is a re-run of the wizard.
self.hasRunWizard = Settings().value(u'general/has run wizard')
self.hasRunWizard = Settings().value(u'core/has run wizard')
# Sort out internet access for downloads
if self.web_access:
songs = self.config.get(u'songs', u'languages')
@ -252,7 +252,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
self.application.set_busy_cursor()
self._performWizard()
self.application.set_normal_cursor()
Settings().setValue(u'general/has run wizard', True)
Settings().setValue(u'core/has run wizard', True)
self.close()
def urlGetFile(self, url, fpath):
@ -459,7 +459,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
self.urlGetFile(u'%s%s' % (self.web, theme), os.path.join(themes_destination, theme))
# Set Default Display
if self.displayComboBox.currentIndex() != -1:
Settings().setValue(u'General/monitor', self.displayComboBox.currentIndex())
Settings().setValue(u'core/monitor', self.displayComboBox.currentIndex())
self.screens.set_current_display(self.displayComboBox.currentIndex())
# Set Global Theme
if self.themeComboBox.currentIndex() != -1:

View File

@ -49,7 +49,7 @@ class GeneralTab(SettingsTab):
self.screens = ScreenList()
self.icon_path = u':/icon/openlp-logo-16x16.png'
general_translated = translate('OpenLP.GeneralTab', 'General')
SettingsTab.__init__(self, parent, u'General', general_translated)
SettingsTab.__init__(self, parent, u'Core', general_translated)
def setupUi(self):
"""

View File

@ -357,7 +357,7 @@ class MainDisplay(Display):
# Single screen active
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if Settings().value(u'general/display on monitor'):
if Settings().value(u'core/display on monitor'):
self.setVisible(True)
else:
self.setVisible(True)
@ -405,7 +405,7 @@ class MainDisplay(Display):
self.footer(service_item.foot_text)
# if was hidden keep it hidden
if self.hide_mode and self.is_live and not service_item.is_media():
if Settings().value(u'general/auto unblank'):
if Settings().value(u'core/auto unblank'):
Registry().execute(u'slidecontroller_live_unblank')
else:
self.hide_display(self.hide_mode)
@ -427,7 +427,7 @@ class MainDisplay(Display):
log.debug(u'hide_display mode = %d', mode)
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if not Settings().value(u'general/display on monitor'):
if not Settings().value(u'core/display on monitor'):
return
if mode == HideMode.Screen:
self.frame.evaluateJavaScript(u'show_blank("desktop");')
@ -450,7 +450,7 @@ class MainDisplay(Display):
log.debug(u'show_display')
if self.screens.display_count == 1:
# Only make visible if setting enabled.
if not Settings().value(u'general/display on monitor'):
if not Settings().value(u'core/display on monitor'):
return
self.frame.evaluateJavaScript('show_blank("show");')
if self.isHidden():

View File

@ -476,7 +476,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.arguments = self.application.args
# Set up settings sections for the main application (not for use by plugins).
self.ui_settings_section = u'user interface'
self.general_settings_section = u'general'
self.general_settings_section = u'core'
self.advanced_settings_section = u'advanced'
self.shortcuts_settings_section = u'shortcuts'
self.service_manager_settings_section = u'servicemanager'
@ -491,7 +491,6 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.new_data_path = None
self.copy_data = False
Settings().set_up_default_values()
Settings().remove_obsolete_settings()
self.service_not_saved = False
self.about_form = AboutForm(self)
self.media_controller = MediaController()

View File

@ -415,7 +415,7 @@ class MediaController(object):
elif not hidden or controller.media_info.is_background or service_item.will_auto_start:
autoplay = True
# Unblank on load set
elif Settings().value(u'general/auto unblank'):
elif Settings().value(u'core/auto unblank'):
autoplay = True
if autoplay:
if not self.media_play(controller):

View File

@ -273,7 +273,6 @@ class ServiceManagerDialog(object):
Registry().register_function(u'config_screen_changed', self.regenerate_service_Items)
Registry().register_function(u'theme_update_global', self.theme_change)
Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes)
Registry().register_function(u'servicemanager_set_item', self.on_set_item)
def drag_enter_event(self, event):
"""
@ -315,6 +314,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
self.layout.setSpacing(0)
self.layout.setMargin(0)
self.setup_ui(self)
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item)
def set_modified(self, modified=True):
"""
@ -993,7 +994,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
def on_set_item(self, message):
"""
Called by a signal to select a specific item.
Called by a signal to select a specific item and make it live usually from remote.
"""
self.set_item(int(message))

View File

@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog):
"""
Process the form saving the settings
"""
log.debug(u'Processing settings exit')
for tabIndex in range(self.stacked_layout.count()):
self.stacked_layout.widget(tabIndex).save()
# if the display of image background are changing we need to regenerate the image cache

View File

@ -44,6 +44,8 @@ from openlp.core.utils.actions import ActionList, CategoryOrder
log = logging.getLogger(__name__)
# Threshold which has to be trespassed to toggle.
HIDE_MENU_THRESHOLD = 27
AUDIO_TIME_LABEL_STYLESHEET = u'background-color: palette(background); ' \
u'border-top-color: palette(shadow); ' \
u'border-left-color: palette(shadow); ' \
@ -358,8 +360,9 @@ class SlideController(DisplayController):
# Signals
self.preview_list_widget.clicked.connect(self.onSlideSelected)
if self.is_live:
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay)
Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display)
self.toolbar.set_widget_visible(self.loop_list, False)
self.toolbar.set_widget_visible(self.wide_menu, False)
else:
@ -371,13 +374,16 @@ class SlideController(DisplayController):
else:
self.preview_list_widget.addActions([self.nextItem, self.previous_item])
Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
Registry().register_function(u'slidecontroller_%s_next' % self.type_prefix, self.on_slide_selected_next)
Registry().register_function(u'slidecontroller_%s_previous' % self.type_prefix, self.on_slide_selected_previous)
Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
Registry().register_function(u'slidecontroller_%s_set' % self.type_prefix, self.on_slide_selected_index)
Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
Registry().register_function(u'slidecontroller_%s_unblank' % self.type_prefix, self.on_slide_unblank)
Registry().register_function(u'slidecontroller_update_slide_limits', self.update_slide_limits)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix),
self.on_slide_selected_index)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_next' % self.type_prefix),
self.on_slide_selected_next)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_previous' % self.type_prefix),
self.on_slide_selected_previous)
def _slideShortcutActivated(self):
"""
@ -588,12 +594,12 @@ class SlideController(DisplayController):
if self.is_live:
# Space used by the toolbar.
used_space = self.toolbar.size().width() + self.hide_menu.size().width()
# The + 40 is needed to prevent flickering. This can be considered a "buffer".
if width > used_space + 40 and self.hide_menu.isVisible():
# Add the threshold to prevent flickering.
if width > used_space + HIDE_MENU_THRESHOLD and self.hide_menu.isVisible():
self.toolbar.set_widget_visible(self.narrow_menu, False)
self.toolbar.set_widget_visible(self.wide_menu)
# The - 40 is needed to prevent flickering. This can be considered a "buffer".
elif width < used_space - 40 and not self.hide_menu.isVisible():
# Take away a threshold to prevent flickering.
elif width < used_space - HIDE_MENU_THRESHOLD and not self.hide_menu.isVisible():
self.toolbar.set_widget_visible(self.wide_menu, False)
self.toolbar.set_widget_visible(self.narrow_menu)

View File

@ -44,7 +44,7 @@ from openlp.core.lib.theme import ThemeXML, BackgroundType, VerticalType, Backgr
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.theme import Theme
from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.utils import AppLocation, delete_file, locale_compare, get_filesystem_encoding
from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_filesystem_encoding
log = logging.getLogger(__name__)
@ -418,7 +418,7 @@ class ThemeManager(QtGui.QWidget):
self.theme_list_widget.clear()
files = AppLocation.get_files(self.settings_section, u'.png')
# Sort the themes by its name considering language specific
files.sort(key=lambda file_name: unicode(file_name), cmp=locale_compare)
files.sort(key=lambda file_name: get_locale_key(unicode(file_name)))
# now process the file list of png files
for name in files:
# check to see file is in theme root directory

View File

@ -38,6 +38,7 @@ import re
from subprocess import Popen, PIPE
import sys
import urllib2
import icu
from PyQt4 import QtGui, QtCore
@ -56,10 +57,12 @@ from openlp.core.lib import translate
log = logging.getLogger(__name__)
APPLICATION_VERSION = {}
IMAGES_FILTER = None
ICU_COLLATOR = None
UNO_CONNECTION_TYPE = u'pipe'
#UNO_CONNECTION_TYPE = u'socket'
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
class VersionThread(QtCore.QThread):
@ -184,7 +187,7 @@ def check_latest_version(current_version):
settings = Settings()
settings.beginGroup(u'general')
last_test = settings.value(u'last version test')
this_test = datetime.now().date()
this_test = unicode(datetime.now().date())
settings.setValue(u'last version test', this_test)
settings.endGroup()
# Tell the main window whether there will ever be data to display
@ -244,8 +247,7 @@ def get_images_filter():
global IMAGES_FILTER
if not IMAGES_FILTER:
log.debug(u'Generating images filter.')
formats = [unicode(fmt)
for fmt in QtGui.QImageReader.supportedImageFormats()]
formats = map(unicode, QtGui.QImageReader.supportedImageFormats())
visible_formats = u'(*.%s)' % u'; *.'.join(formats)
actual_formats = u'(*.%s)' % u' *.'.join(formats)
IMAGES_FILTER = u'%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
@ -379,21 +381,32 @@ def format_time(text, local_time):
return re.sub('\%[a-zA-Z]', match_formatting, text)
def locale_compare(string1, string2):
def get_locale_key(string):
"""
Compares two strings according to the current locale settings.
As any other compare function, returns a negative, or a positive value,
or 0, depending on whether string1 collates before or after string2 or
is equal to it. Comparison is case insensitive.
Creates a key for case insensitive, locale aware string sorting.
"""
# Function locale.strcoll() from standard Python library does not work properly on Windows.
return locale.strcoll(string1.lower(), string2.lower())
string = string.lower()
# For Python 3 on platforms other than Windows ICU is not necessary. In those cases locale.strxfrm(str) can be used.
global ICU_COLLATOR
if ICU_COLLATOR is None:
from languagemanager import LanguageManager
locale = LanguageManager.get_language()
icu_locale = icu.Locale(locale)
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
return ICU_COLLATOR.getSortKey(string)
# For performance reasons provide direct reference to compare function without wrapping it in another function making
# the string lowercase. This is needed for sorting songs.
locale_direct_compare = locale.strcoll
def get_natural_key(string):
"""
Generate a key for locale aware natural string sorting.
Returns a list of string compare keys and integers.
"""
key = DIGITS_OR_NONDIGITS.findall(string)
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
# Python 3 does not support comparision of different types anymore. So make sure, that we do not compare str and int.
#if string[0].isdigit():
# return [''] + key
return key
from applocation import AppLocation
@ -403,4 +416,4 @@ from actions import ActionList
__all__ = [u'AppLocation', u'ActionList', u'LanguageManager', u'get_application_version', u'check_latest_version',
u'add_actions', u'get_filesystem_encoding', u'get_web_page', u'get_uno_command', u'get_uno_instance',
u'delete_file', u'clean_filename', u'format_time', u'locale_compare', u'locale_direct_compare']
u'delete_file', u'clean_filename', u'format_time', u'get_locale_key', u'get_natural_key']

View File

@ -98,7 +98,7 @@ class LanguageManager(object):
"""
Retrieve a saved language to use from settings
"""
language = Settings().value(u'general/language')
language = Settings().value(u'core/language')
language = str(language)
log.info(u'Language file: \'%s\' Loaded from conf file' % language)
if re.match(r'[[].*[]]', language):
@ -128,7 +128,7 @@ class LanguageManager(object):
language = unicode(qm_list[action_name])
if LanguageManager.auto_language:
language = u'[%s]' % language
Settings().setValue(u'general/language', language)
Settings().setValue(u'core/language', language)
log.info(u'Language file: \'%s\' written to conf file' % language)
if message:
QtGui.QMessageBox.information(None,

View File

@ -49,10 +49,12 @@ class AlertsManager(QtCore.QObject):
def __init__(self, parent):
QtCore.QObject.__init__(self, parent)
Registry().register(u'alerts_manager', self)
self.timer_id = 0
self.alert_list = []
Registry().register_function(u'live_display_active', self.generate_alert)
Registry().register_function(u'alerts_text', self.alert_text)
QtCore.QObject.connect(self, QtCore.SIGNAL(u'alerts_text'), self.alert_text)
def alert_text(self, message):
"""

View File

@ -38,7 +38,7 @@ from openlp.core.lib import Settings, UiStrings, translate
from openlp.core.lib.db import delete_database
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
from openlp.core.utils import AppLocation, locale_compare
from openlp.core.utils import AppLocation, get_locale_key
from openlp.plugins.bibles.lib.manager import BibleFormat
from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename
@ -455,7 +455,7 @@ class BibleImportForm(OpenLPWizard):
"""
self.webTranslationComboBox.clear()
bibles = self.web_bible_list[index].keys()
bibles.sort(cmp=locale_compare)
bibles.sort(key=get_locale_key)
self.webTranslationComboBox.addItems(bibles)
def onOsisBrowseButtonClicked(self):

View File

@ -714,6 +714,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
Registry().get(u'application').process_events()
return soup
def send_error_message(error_type):
"""
Send a standard error message informing the user of an issue.

View File

@ -36,7 +36,7 @@ from openlp.core.lib import Registry, MediaManagerItem, ItemCapabilities, Servic
from openlp.core.lib.searchedit import SearchEdit
from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \
critical_error_message_box, find_and_set_in_combo_box, build_icon
from openlp.core.utils import locale_compare
from openlp.core.utils import get_locale_key
from openlp.plugins.bibles.forms import BibleImportForm, EditBibleForm
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
LanguageSelection, BibleStrings
@ -325,7 +325,7 @@ class BibleMediaItem(MediaManagerItem):
# Get all bibles and sort the list.
bibles = self.plugin.manager.get_bibles().keys()
bibles = filter(None, bibles)
bibles.sort(cmp=locale_compare)
bibles.sort(key=get_locale_key)
# Load the bibles into the combo boxes.
self.quickVersionComboBox.addItems(bibles)
self.quickSecondComboBox.addItems(bibles)
@ -461,7 +461,7 @@ class BibleMediaItem(MediaManagerItem):
for book in book_data:
data = BiblesResourcesDB.get_book_by_id(book.book_reference_id)
books.append(data[u'name'] + u' ')
books.sort(cmp=locale_compare)
books.sort(key=get_locale_key)
set_case_insensitive_completer(books, self.quickSearchEdit)
def on_import_click(self):

View File

@ -35,7 +35,7 @@ from sqlalchemy import Column, Table, types
from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import locale_compare
from openlp.core.utils import get_locale_key
class CustomSlide(BaseModel):
"""
@ -44,11 +44,10 @@ class CustomSlide(BaseModel):
# By default sort the customs by its title considering language specific
# characters.
def __lt__(self, other):
r = locale_compare(self.title, other.title)
return True if r < 0 else False
return get_locale_key(self.title) < get_locale_key(other.title)
def __eq__(self, other):
return 0 == locale_compare(self.title, other.title)
return get_locale_key(self.title) == get_locale_key(other.title)
def init_schema(url):

View File

@ -27,6 +27,6 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`images` module provides the Images plugin. The Images plugin
provides the facility to display images from OpenLP.
The :mod:`images` module provides the Images plugin. The Images plugin provides the facility to display images from
OpenLP.
"""

View File

@ -27,20 +27,16 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Forms in OpenLP are made up of two classes. One class holds all the graphical
elements, like buttons and lists, and the other class holds all the functional
code, like slots and loading and saving.
Forms in OpenLP are made up of two classes. One class holds all the graphical elements, like buttons and lists, and the
other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named
``Ui_<name>Dialog``. It is a slightly modified version of the class that the
``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()``
function for translating strings.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named
``<name>Form``. This class is the one which is instantiated and used. It uses
dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class
mentioned above, like so::
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned
above, like so::
class AuthorsForm(QtGui.QDialog, Ui_AuthorsDialog):
@ -48,9 +44,8 @@ mentioned above, like so::
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
This allows OpenLP to use ``self.object`` for all the GUI elements while keeping
them separate from the functionality, so that it is easier to recreate the GUI
from the .ui files later if necessary.
This allows OpenLP to use ``self.object`` for all the GUI elements while keeping them separate from the functionality,
so that it is easier to recreate the GUI from the .ui files later if necessary.
"""
from addgroupform import AddGroupForm

View File

@ -47,16 +47,16 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
def exec_(self, clear=True, show_top_level_group=False, selected_group=None):
"""
Show the form
Show the form.
``clear``
Set to False if the text input box should not be cleared when showing the dialog (default: True)
Set to False if the text input box should not be cleared when showing the dialog (default: True).
``show_top_level_group``
Set to True when "-- Top level group --" should be showed as first item (default: False)
Set to True when "-- Top level group --" should be showed as first item (default: False).
``selected_group``
The ID of the group that should be selected by default when showing the dialog
The ID of the group that should be selected by default when showing the dialog.
"""
if clear:
self.name_edit.clear()
@ -72,7 +72,7 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
def accept(self):
"""
Override the accept() method from QDialog to make sure something is entered in the text input box
Override the accept() method from QDialog to make sure something is entered in the text input box.
"""
if not self.name_edit.text():
critical_error_message_box(message=translate('ImagePlugin.AddGroupForm',

View File

@ -48,10 +48,10 @@ class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog):
Show the form
``selected_group``
The ID of the group that should be selected by default when showing the dialog
The ID of the group that should be selected by default when showing the dialog.
"""
if selected_group is not None:
for i in range(self.group_combobox.count()):
if self.group_combobox.itemData(i) == selected_group:
self.group_combobox.setCurrentIndex(i)
for index in range(self.group_combobox.count()):
if self.group_combobox.itemData(index) == selected_group:
self.group_combobox.setCurrentIndex(index)
return QtGui.QDialog.exec_(self)

View File

@ -70,10 +70,10 @@ class ImagePlugin(Plugin):
def app_startup(self):
"""
Perform tasks on application startup
Perform tasks on application startup.
"""
Plugin.app_startup(self)
# Convert old settings-based image list to the database
# Convert old settings-based image list to the database.
files_from_config = Settings().get_files_from_config(self)
if files_from_config:
log.debug(u'Importing images list from old config: %s' % files_from_config)
@ -93,7 +93,7 @@ class ImagePlugin(Plugin):
def set_plugin_text_strings(self):
"""
Called to define all translatable texts of the plugin
Called to define all translatable texts of the plugin.
"""
## Name PluginList ##
self.text_strings[StringContent.Name] = {
@ -117,8 +117,8 @@ class ImagePlugin(Plugin):
def config_update(self):
"""
Triggered by saving and changing the image border. Sets the images in image manager to require updates.
Actual update is triggered by the last part of saving the config.
Triggered by saving and changing the image border. Sets the images in image manager to require updates. Actual
update is triggered by the last part of saving the config.
"""
log.info(u'Images config_update')
background = QtGui.QColor(Settings().value(self.settings_section + u'/background color'))

View File

@ -27,7 +27,7 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`db` module provides the database and schema that is the backend for the Images plugin
The :mod:`db` module provides the database and schema that is the backend for the Images plugin.
"""
from sqlalchemy import Column, ForeignKey, Table, types
@ -38,14 +38,14 @@ from openlp.core.lib.db import BaseModel, init_db
class ImageGroups(BaseModel):
"""
ImageGroups model
ImageGroups model.
"""
pass
class ImageFilenames(BaseModel):
"""
ImageFilenames model
ImageFilenames model.
"""
pass

View File

@ -36,10 +36,11 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, Servic
StringContent, TreeWidgetWithDnD, UiStrings, build_icon, check_directory_exists, check_item_selected, \
create_thumb, translate, validate_thumb
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.utils import AppLocation, delete_file, locale_compare, get_images_filter
from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_images_filter
from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
log = logging.getLogger(__name__)
@ -60,24 +61,23 @@ class ImageMediaItem(MediaManagerItem):
self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
Registry().register_function(u'live_theme_changed', self.live_theme_changed)
# Allow DnD from the desktop
# Allow DnD from the desktop.
self.list_view.activateDnD()
def retranslateUi(self):
self.on_new_prompt = translate('ImagePlugin.MediaItem',
'Select Image(s)')
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
file_formats = get_images_filter()
self.on_new_file_masks = u'%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles)
self.addGroupAction.setText(UiStrings().AddGroup)
self.addGroupAction.setToolTip(UiStrings().AddGroup)
self.replaceAction.setText(UiStrings().ReplaceBG)
self.replaceAction.setToolTip(UiStrings().ReplaceLiveBG)
self.resetAction.setText(UiStrings().ResetBG)
self.resetAction.setToolTip(UiStrings().ResetLiveBG)
self.replace_action.setText(UiStrings().ReplaceBG)
self.replace_action.setToolTip(UiStrings().ReplaceLiveBG)
self.reset_action.setText(UiStrings().ResetBG)
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
def required_icons(self):
"""
Set which icons the media manager tab should show
Set which icons the media manager tab should show.
"""
MediaManagerItem.required_icons(self)
self.has_file_icon = True
@ -94,13 +94,13 @@ class ImageMediaItem(MediaManagerItem):
self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settings_section), u'thumbnails')
check_directory_exists(self.servicePath)
# Load images from the database
self.loadFullList(
self.load_full_list(
self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True)
def add_list_view_to_toolbar(self):
"""
Creates the main widget for listing items the media item is tracking.
This method overloads MediaManagerItem.add_list_view_to_toolbar
Creates the main widget for listing items the media item is tracking. This method overloads
MediaManagerItem.add_list_view_to_toolbar.
"""
# Add the List widget
self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
@ -155,44 +155,41 @@ class ImageMediaItem(MediaManagerItem):
self.list_view.doubleClicked.connect(self.on_double_clicked)
self.list_view.itemSelectionChanged.connect(self.on_selection_change)
self.list_view.customContextMenuRequested.connect(self.context_menu)
self.list_view.addAction(self.replaceAction)
self.list_view.addAction(self.replace_action)
def add_custom_context_actions(self):
"""
Add custom actions to the context menu
Add custom actions to the context menu.
"""
create_widget_action(self.list_view, separator=True)
create_widget_action(self.list_view,
text=UiStrings().AddGroup,
icon=u':/images/image_new_group.png',
triggers=self.onAddGroupClick)
text=UiStrings().AddGroup, icon=u':/images/image_new_group.png', triggers=self.on_add_group_click)
create_widget_action(self.list_view,
text=self.plugin.get_string(StringContent.Load)[u'tooltip'],
icon=u':/general/general_open.png',
triggers=self.on_file_click)
icon=u':/general/general_open.png', triggers=self.on_file_click)
def add_start_header_bar(self):
"""
Add custom buttons to the start of the toolbar
Add custom buttons to the start of the toolbar.
"""
self.addGroupAction = self.toolbar.add_toolbar_action(u'addGroupAction',
icon=u':/images/image_new_group.png', triggers=self.onAddGroupClick)
icon=u':/images/image_new_group.png', triggers=self.on_add_group_click)
def add_end_header_bar(self):
"""
Add custom buttons to the end of the toolbar
"""
self.replaceAction = self.toolbar.add_toolbar_action(u'replaceAction',
icon=u':/slides/slide_blank.png', triggers=self.onReplaceClick)
self.resetAction = self.toolbar.add_toolbar_action(u'resetAction',
icon=u':/system/system_close.png', visible=False, triggers=self.onResetClick)
self.replace_action = self.toolbar.add_toolbar_action(u'replace_action',
icon=u':/slides/slide_blank.png', triggers=self.on_replace_click)
self.reset_action = self.toolbar.add_toolbar_action(u'reset_action',
icon=u':/system/system_close.png', visible=False, triggers=self.on_reset_click)
def recursively_delete_group(self, image_group):
"""
Recursively deletes a group and all groups and images in it
Recursively deletes a group and all groups and images in it.
``image_group``
The ImageGroups instance of the group that will be deleted
The ImageGroups instance of the group that will be deleted.
"""
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
for image in images:
@ -205,7 +202,7 @@ class ImageMediaItem(MediaManagerItem):
def on_delete_click(self):
"""
Remove an image item from the list
Remove an image item from the list.
"""
# Turn off auto preview triggers.
self.list_view.blockSignals(True)
@ -226,11 +223,11 @@ class ImageMediaItem(MediaManagerItem):
self.manager.delete_object(ImageFilenames, row_item.data(0, QtCore.Qt.UserRole).id)
elif isinstance(item_data, ImageGroups):
if QtGui.QMessageBox.question(self.list_view.parent(),
translate('ImagePlugin.MediaItem', 'Remove group'),
translate('ImagePlugin.MediaItem',
'Are you sure you want to remove "%s" and everything in it?') % item_data.group_name,
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No)) == QtGui.QMessageBox.Yes:
translate('ImagePlugin.MediaItem', 'Remove group'),
translate('ImagePlugin.MediaItem',
'Are you sure you want to remove "%s" and everything in it?') % item_data.group_name,
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No)) == QtGui.QMessageBox.Yes:
self.recursively_delete_group(item_data)
self.manager.delete_object(ImageGroups, row_item.data(0, QtCore.Qt.UserRole).id)
if item_data.parent_id == 0:
@ -246,16 +243,16 @@ class ImageMediaItem(MediaManagerItem):
def add_sub_groups(self, group_list, parent_group_id):
"""
Recursively add subgroups to the given parent group in a QTreeWidget
Recursively add subgroups to the given parent group in a QTreeWidget.
``group_list``
The List object that contains all QTreeWidgetItems
The List object that contains all QTreeWidgetItems.
``parent_group_id``
The ID of the group that will be added recursively
The ID of the group that will be added recursively.
"""
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
folder_icon = build_icon(u':/images/image_group.png')
for image_group in image_groups:
group = QtGui.QTreeWidgetItem()
@ -271,35 +268,35 @@ class ImageMediaItem(MediaManagerItem):
def fill_groups_combobox(self, combobox, parent_group_id=0, prefix=''):
"""
Recursively add groups to the combobox in the 'Add group' dialog
Recursively add groups to the combobox in the 'Add group' dialog.
``combobox``
The QComboBox to add the options to
The QComboBox to add the options to.
``parent_group_id``
The ID of the group that will be added
The ID of the group that will be added.
``prefix``
A string containing the prefix that will be added in front of the groupname for each level of the tree
A string containing the prefix that will be added in front of the groupname for each level of the tree.
"""
if parent_group_id == 0:
combobox.clear()
combobox.top_level_group_added = False
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
for image_group in image_groups:
combobox.addItem(prefix + image_group.group_name, image_group.id)
self.fill_groups_combobox(combobox, image_group.id, prefix + ' ')
def expand_group(self, group_id, root_item=None):
"""
Expand groups in the widget recursively
Expand groups in the widget recursively.
``group_id``
The ID of the group that will be expanded
The ID of the group that will be expanded.
``root_item``
This option is only used for recursion purposes
This option is only used for recursion purposes.
"""
return_value = False
if root_item is None:
@ -314,31 +311,31 @@ class ImageMediaItem(MediaManagerItem):
return True
return return_value
def loadFullList(self, images, initial_load=False, open_group=None):
def load_full_list(self, images, initial_load=False, open_group=None):
"""
Replace the list of images and groups in the interface.
``images``
A List of ImageFilenames objects that will be used to reload the mediamanager list
A List of ImageFilenames objects that will be used to reload the mediamanager list.
``initial_load``
When set to False, the busy cursor and progressbar will be shown while loading images
When set to False, the busy cursor and progressbar will be shown while loading images.
``open_group``
ImageGroups object of the group that must be expanded after reloading the list in the interface
ImageGroups object of the group that must be expanded after reloading the list in the interface.
"""
if not initial_load:
self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(images))
self.list_view.clear()
# Load the list of groups and add them to the treeView
# Load the list of groups and add them to the treeView.
group_items = {}
self.add_sub_groups(group_items, parent_group_id=0)
if open_group is not None:
self.expand_group(open_group.id)
# Sort the images by its filename considering language specific
# Sort the images by its filename considering language specific.
# characters.
images.sort(cmp=locale_compare, key=lambda image_object: os.path.split(unicode(image_object.filename))[1])
images.sort(key=lambda image_object: get_locale_key(os.path.split(unicode(image_object.filename))[1]))
for imageFile in images:
log.debug(u'Loading image: %s', imageFile.filename)
filename = os.path.split(imageFile.filename)[1]
@ -455,7 +452,7 @@ class ImageMediaItem(MediaManagerItem):
self.main_window.display_progress_bar(len(images))
# Save the new images in the database
self.save_new_images_list(images, group_id=parent_group.id, reload_list=False)
self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
initial_load=initial_load, open_group=parent_group)
self.application.set_normal_cursor()
@ -482,7 +479,7 @@ class ImageMediaItem(MediaManagerItem):
self.manager.save_object(imageFile)
self.main_window.increment_progress_bar()
if reload_list and images_list:
self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
def dnd_move_internal(self, target):
"""
@ -525,12 +522,12 @@ class ImageMediaItem(MediaManagerItem):
group_items.append(item)
if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
image_items.append(item)
group_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
group_items.sort(key=lambda item: get_locale_key(item.text(0)))
target_group.addChildren(group_items)
image_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
image_items.sort(key=lambda item: get_locale_key(item.text(0)))
target_group.addChildren(image_items)
def generate_slide_data(self, service_item, item=None, xmlVersion=False,
def generate_slide_data(self, service_item, item=None, xml_version=False,
remote=False, context=ServiceItemContext.Service):
"""
Generate the slide data. Needs to be implemented by the plugin.
@ -608,7 +605,7 @@ class ImageMediaItem(MediaManagerItem):
else:
return False
def onAddGroupClick(self):
def on_add_group_click(self):
"""
Called to add a new group
"""
@ -629,7 +626,7 @@ class ImageMediaItem(MediaManagerItem):
group_name=self.add_group_form.name_edit.text())
if not self.check_group_exists(new_group):
if self.manager.save_object(new_group):
self.loadFullList(self.manager.get_all_objects(ImageFilenames,
self.load_full_list(self.manager.get_all_objects(ImageFilenames,
order_by_ref=ImageFilenames.filename))
self.expand_group(new_group.id)
self.fill_groups_combobox(self.choose_group_form.group_combobox)
@ -638,23 +635,22 @@ class ImageMediaItem(MediaManagerItem):
critical_error_message_box(
message=translate('ImagePlugin.AddGroupForm', 'Could not add the new group.'))
else:
critical_error_message_box(
message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
critical_error_message_box(message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
def onResetClick(self):
def on_reset_click(self):
"""
Called to reset the Live background with the image selected,
Called to reset the Live background with the image selected.
"""
self.resetAction.setVisible(False)
self.reset_action.setVisible(False)
self.live_controller.display.reset_image()
def live_theme_changed(self):
"""
Triggered by the change of theme in the slide controller
Triggered by the change of theme in the slide controller.
"""
self.resetAction.setVisible(False)
self.reset_action.setVisible(False)
def onReplaceClick(self):
def on_replace_click(self):
"""
Called to replace Live backgound with the image selected.
"""
@ -663,12 +659,12 @@ class ImageMediaItem(MediaManagerItem):
background = QtGui.QColor(Settings().value(self.settings_section + u'/background color'))
bitem = self.list_view.selectedItems()[0]
if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
# Only continue when an image is selected
# Only continue when an image is selected.
return
filename = bitem.data(0, QtCore.Qt.UserRole).filename
if os.path.exists(filename):
if self.live_controller.display.direct_image(filename, background):
self.resetAction.setVisible(True)
self.reset_action.setVisible(True)
else:
critical_error_message_box(UiStrings().LiveBGError,
translate('ImagePlugin.MediaItem', 'There was no display item to amend.'))

View File

@ -27,8 +27,7 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`media` module provides the Media plugin which allows OpenLP to
display videos. The media supported depends not only on the Python support
but also extensively on the codecs installed on the underlying operating system
being picked up and usable by Python.
The :mod:`media` module provides the Media plugin which allows OpenLP to display videos. The media supported depends not
only on the Python support but also extensively on the codecs installed on the underlying operating system being picked
up and usable by Python.
"""

View File

@ -37,15 +37,18 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem,MediaType, Regist
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
from openlp.core.ui import DisplayController, Display, DisplayControllerType
from openlp.core.ui.media import get_media_players, set_media_players
from openlp.core.utils import AppLocation, locale_compare
from openlp.core.utils import AppLocation, get_locale_key
log = logging.getLogger(__name__)
CLAPPERBOARD = u':/media/slidecontroller_multimedia.png'
VIDEO = build_icon(QtGui.QImage(u':/media/media_video.png'))
AUDIO = build_icon(QtGui.QImage(u':/media/media_audio.png'))
DVDICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
ERROR = build_icon(QtGui.QImage(u':/general/general_delete.png'))
VIDEO_ICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
AUDIO_ICON = build_icon(QtGui.QImage(u':/media/media_audio.png'))
DVD_ICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
ERROR_ICON = build_icon(QtGui.QImage(u':/general/general_delete.png'))
class MediaMediaItem(MediaManagerItem):
"""
@ -79,12 +82,12 @@ class MediaMediaItem(MediaManagerItem):
def retranslateUi(self):
self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
self.replaceAction.setText(UiStrings().ReplaceBG)
self.replaceAction.setToolTip(UiStrings().ReplaceLiveBG)
self.resetAction.setText(UiStrings().ResetBG)
self.resetAction.setToolTip(UiStrings().ResetLiveBG)
self.replace_action.setText(UiStrings().ReplaceBG)
self.replace_action.setToolTip(UiStrings().ReplaceLiveBG)
self.reset_action.setText(UiStrings().ResetBG)
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
self.automatic = UiStrings().Automatic
self.displayTypeLabel.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
self.rebuild_players()
def required_icons(self):
@ -98,27 +101,28 @@ class MediaMediaItem(MediaManagerItem):
def add_list_view_to_toolbar(self):
MediaManagerItem.add_list_view_to_toolbar(self)
self.list_view.addAction(self.replaceAction)
self.list_view.addAction(self.replace_action)
def add_end_header_bar(self):
# Replace backgrounds do not work at present so remove functionality.
self.replaceAction = self.toolbar.add_toolbar_action(u'replaceAction', icon=u':/slides/slide_blank.png',
self.replace_action = self.toolbar.add_toolbar_action(u'replace_action', icon=u':/slides/slide_blank.png',
triggers=self.onReplaceClick)
self.resetAction = self.toolbar.add_toolbar_action(u'resetAction', icon=u':/system/system_close.png',
self.reset_action = self.toolbar.add_toolbar_action(u'reset_action', icon=u':/system/system_close.png',
visible=False, triggers=self.onResetClick)
self.mediaWidget = QtGui.QWidget(self)
self.mediaWidget.setObjectName(u'mediaWidget')
self.displayLayout = QtGui.QFormLayout(self.mediaWidget)
self.displayLayout.setMargin(self.displayLayout.spacing())
self.displayLayout.setObjectName(u'displayLayout')
self.displayTypeLabel = QtGui.QLabel(self.mediaWidget)
self.displayTypeLabel.setObjectName(u'displayTypeLabel')
self.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.mediaWidget, u'displayTypeComboBox')
self.displayTypeLabel.setBuddy(self.displayTypeComboBox)
self.displayLayout.addRow(self.displayTypeLabel, self.displayTypeComboBox)
# Add the Media widget to the page layout
self.page_layout.addWidget(self.mediaWidget)
self.displayTypeComboBox.currentIndexChanged.connect(self.overridePlayerChanged)
self.media_widget = QtGui.QWidget(self)
self.media_widget.setObjectName(u'media_widget')
self.display_layout = QtGui.QFormLayout(self.media_widget)
self.display_layout.setMargin(self.display_layout.spacing())
self.display_layout.setObjectName(u'display_layout')
self.display_type_label = QtGui.QLabel(self.media_widget)
self.display_type_label.setObjectName(u'display_type_label')
self.display_type_combo_box = create_horizontal_adjusting_combo_box(
self.media_widget, u'display_type_combo_box')
self.display_type_label.setBuddy(self.display_type_combo_box)
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
# Add the Media widget to the page layout.
self.page_layout.addWidget(self.media_widget)
self.display_type_combo_box.currentIndexChanged.connect(self.overridePlayerChanged)
def overridePlayerChanged(self, index):
player = get_media_players()[0]
@ -132,13 +136,13 @@ class MediaMediaItem(MediaManagerItem):
Called to reset the Live background with the media selected,
"""
self.media_controller.media_reset(self.live_controller)
self.resetAction.setVisible(False)
self.reset_action.setVisible(False)
def video_background_replaced(self):
"""
Triggered by main display on change of serviceitem.
"""
self.resetAction.setVisible(False)
self.reset_action.setVisible(False)
def onReplaceClick(self):
"""
@ -155,7 +159,7 @@ class MediaMediaItem(MediaManagerItem):
(path, name) = os.path.split(filename)
service_item.add_from_command(path, name,CLAPPERBOARD)
if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True):
self.resetAction.setVisible(True)
self.reset_action.setVisible(True)
else:
critical_error_message_box(UiStrings().LiveBGError,
translate('MediaPlugin.MediaItem', 'There was no display item to amend.'))
@ -164,7 +168,7 @@ class MediaMediaItem(MediaManagerItem):
translate('MediaPlugin.MediaItem',
'There was a problem replacing your background, the media file "%s" no longer exists.') % filename)
def generate_slide_data(self, service_item, item=None, xmlVersion=False, remote=False,
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
context=ServiceItemContext.Live):
"""
Generate the slide data. Needs to be implemented by the plugin.
@ -181,7 +185,7 @@ class MediaMediaItem(MediaManagerItem):
translate('MediaPlugin.MediaItem', 'Missing Media File'),
translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename)
return False
service_item.title = self.displayTypeComboBox.currentText()
service_item.title = self.display_type_combo_box.currentText()
service_item.shortname = service_item.title
(path, name) = os.path.split(filename)
service_item.add_from_command(path, name, CLAPPERBOARD)
@ -209,8 +213,7 @@ class MediaMediaItem(MediaManagerItem):
def rebuild_players(self):
"""
Rebuild the tab in the media manager when changes are made in
the settings
Rebuild the tab in the media manager when changes are made in the settings.
"""
self.populateDisplayTypes()
self.on_new_file_masks = translate('MediaPlugin.MediaItem', 'Videos (%s);;Audio (%s);;%s (*)') % (
@ -222,29 +225,27 @@ class MediaMediaItem(MediaManagerItem):
def populateDisplayTypes(self):
"""
Load the combobox with the enabled media players,
allowing user to select a specific player if settings allow
Load the combobox with the enabled media players, allowing user to select a specific player if settings allow.
"""
# block signals to avoid unnecessary overridePlayerChanged Signals
# while combo box creation
self.displayTypeComboBox.blockSignals(True)
self.displayTypeComboBox.clear()
# block signals to avoid unnecessary overridePlayerChanged Signals while combo box creation
self.display_type_combo_box.blockSignals(True)
self.display_type_combo_box.clear()
usedPlayers, overridePlayer = get_media_players()
media_players = self.media_controller.media_players
currentIndex = 0
for player in usedPlayers:
# load the drop down selection
self.displayTypeComboBox.addItem(media_players[player].original_name)
self.display_type_combo_box.addItem(media_players[player].original_name)
if overridePlayer == player:
currentIndex = len(self.displayTypeComboBox)
if self.displayTypeComboBox.count() > 1:
self.displayTypeComboBox.insertItem(0, self.automatic)
self.displayTypeComboBox.setCurrentIndex(currentIndex)
currentIndex = len(self.display_type_combo_box)
if self.display_type_combo_box.count() > 1:
self.display_type_combo_box.insertItem(0, self.automatic)
self.display_type_combo_box.setCurrentIndex(currentIndex)
if overridePlayer:
self.mediaWidget.show()
self.media_widget.show()
else:
self.mediaWidget.hide()
self.displayTypeComboBox.blockSignals(False)
self.media_widget.hide()
self.display_type_combo_box.blockSignals(False)
def on_delete_click(self):
"""
@ -261,40 +262,40 @@ class MediaMediaItem(MediaManagerItem):
def load_list(self, media, target_group=None):
# Sort the media by its filename considering language specific
# characters.
media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
for track in media:
track_info = QtCore.QFileInfo(track)
if not os.path.exists(track):
filename = os.path.split(unicode(track))[1]
item_name = QtGui.QListWidgetItem(filename)
item_name.setIcon(ERROR)
item_name.setIcon(ERROR_ICON)
item_name.setData(QtCore.Qt.UserRole, track)
elif track_info.isFile():
filename = os.path.split(unicode(track))[1]
item_name = QtGui.QListWidgetItem(filename)
if u'*.%s' % (filename.split(u'.')[-1].lower()) in self.media_controller.audio_extensions_list:
item_name.setIcon(AUDIO)
item_name.setIcon(AUDIO_ICON)
else:
item_name.setIcon(VIDEO)
item_name.setIcon(VIDEO_ICON)
item_name.setData(QtCore.Qt.UserRole, track)
else:
filename = os.path.split(unicode(track))[1]
item_name = QtGui.QListWidgetItem(filename)
item_name.setIcon(build_icon(DVDICON))
item_name.setIcon(build_icon(DVD_ICON))
item_name.setData(QtCore.Qt.UserRole, track)
item_name.setToolTip(track)
self.list_view.addItem(item_name)
def getList(self, type=MediaType.Audio):
def get_list(self, type=MediaType.Audio):
media = Settings().value(self.settings_section + u'/media files')
media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
ext = []
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
extension = []
if type == MediaType.Audio:
ext = self.media_controller.audio_extensions_list
extension = self.media_controller.audio_extensions_list
else:
ext = self.media_controller.video_extensions_list
ext = map(lambda x: x[1:], ext)
media = filter(lambda x: os.path.splitext(x)[1] in ext, media)
extension = self.media_controller.video_extensions_list
extension = map(lambda x: x[1:], extension)
media = filter(lambda x: os.path.splitext(x)[1] in extension, media)
return media
def search(self, string, showError):

View File

@ -34,12 +34,14 @@ from PyQt4 import QtCore
from openlp.core.lib import Plugin, Registry, StringContent, Settings, build_icon, translate
from openlp.plugins.media.lib import MediaMediaItem, MediaTab
log = logging.getLogger(__name__)
# Some settings starting with "media" are in core, because they are needed for core functionality.
__default_settings__ = {
u'media/media auto start': QtCore.Qt.Unchecked,
u'media/media files': []
u'media/media auto start': QtCore.Qt.Unchecked,
u'media/media files': []
}
@ -54,7 +56,7 @@ class MediaPlugin(Plugin):
# passed with drag and drop messages
self.dnd_id = u'Media'
def create_settings_Tab(self, parent):
def create_settings_tab(self, parent):
"""
Create the settings Tab
"""
@ -94,7 +96,7 @@ class MediaPlugin(Plugin):
def finalise(self):
"""
Time to tidy up on exit
Time to tidy up on exit.
"""
log.info(u'Media Finalising')
self.media_controller.finalise()
@ -102,19 +104,19 @@ class MediaPlugin(Plugin):
def get_display_css(self):
"""
Add css style sheets to htmlbuilder
Add css style sheets to htmlbuilder.
"""
return self.media_controller.get_media_display_css()
def get_display_javascript(self):
"""
Add javascript functions to htmlbuilder
Add javascript functions to htmlbuilder.
"""
return self.media_controller.get_media_display_javascript()
def get_display_html(self):
"""
Add html code to htmlbuilder
Add html code to htmlbuilder.
"""
return self.media_controller.get_media_display_html()

View File

@ -27,6 +27,6 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`presentations` module provides the Presentations plugin which allows
OpenLP to show presentations from most popular presentation packages.
The :mod:`presentations` module provides the Presentations plugin which allows OpenLP to show presentations from most
popular presentation packages.
"""

View File

@ -62,13 +62,14 @@ from openlp.core.lib import ScreenList
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
from presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)
class ImpressController(PresentationController):
"""
Class to control interactions with Impress presentations.
It creates the runtime environment, loads and closes the presentation as
well as triggering the correct activities based on the users input
Class to control interactions with Impress presentations. It creates the runtime environment, loads and closes the
presentation as well as triggering the correct activities based on the users input.
"""
log.info(u'ImpressController loaded')
@ -79,14 +80,14 @@ class ImpressController(PresentationController):
log.debug(u'Initialising')
PresentationController.__init__(self, plugin, u'Impress', ImpressDocument)
self.supports = [u'odp']
self.alsosupports = [u'ppt', u'pps', u'pptx', u'ppsx']
self.also_supports = [u'ppt', u'pps', u'pptx', u'ppsx']
self.process = None
self.desktop = None
self.manager = None
def check_available(self):
"""
Impress is able to run on this machine
Impress is able to run on this machine.
"""
log.debug(u'check_available')
if os.name == u'nt':
@ -96,9 +97,8 @@ class ImpressController(PresentationController):
def start_process(self):
"""
Loads a running version of OpenOffice in the background.
It is not displayed to the user but is available to the UNO interface
when required.
Loads a running version of OpenOffice in the background. It is not displayed to the user but is available to the
UNO interface when required.
"""
log.debug(u'start process Openoffice')
if os.name == u'nt':
@ -113,8 +113,7 @@ class ImpressController(PresentationController):
def get_uno_desktop(self):
"""
On non-Windows platforms, use Uno. Get the OpenOffice desktop
which will be used to manage impress
On non-Windows platforms, use Uno. Get the OpenOffice desktop which will be used to manage impress.
"""
log.debug(u'get UNO Desktop Openoffice')
uno_instance = None
@ -132,8 +131,7 @@ class ImpressController(PresentationController):
loop += 1
try:
self.manager = uno_instance.ServiceManager
log.debug(u'get UNO Desktop Openoffice - createInstanceWithContext'
u' - Desktop')
log.debug(u'get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance)
return desktop
except:
@ -142,8 +140,7 @@ class ImpressController(PresentationController):
def get_com_desktop(self):
"""
On Windows platforms, use COM. Return the desktop object which
will be used to manage Impress
On Windows platforms, use COM. Return the desktop object which will be used to manage Impress.
"""
log.debug(u'get COM Desktop OpenOffice')
if not self.manager:
@ -157,7 +154,7 @@ class ImpressController(PresentationController):
def get_com_servicemanager(self):
"""
Return the OOo service manager for windows
Return the OOo service manager for windows.
"""
log.debug(u'get_com_servicemanager openoffice')
try:
@ -168,7 +165,7 @@ class ImpressController(PresentationController):
def kill(self):
"""
Called at system exit to clean up any running presentations
Called at system exit to clean up any running presentations.
"""
log.debug(u'Kill OpenOffice')
while self.docs:
@ -203,12 +200,12 @@ class ImpressController(PresentationController):
class ImpressDocument(PresentationDocument):
"""
Class which holds information and controls a single presentation
Class which holds information and controls a single presentation.
"""
def __init__(self, controller, presentation):
"""
Constructor, store information about the file and initialise
Constructor, store information about the file and initialise.
"""
log.debug(u'Init Presentation OpenOffice')
PresentationDocument.__init__(self, controller, presentation)
@ -218,11 +215,9 @@ class ImpressDocument(PresentationDocument):
def load_presentation(self):
"""
Called when a presentation is added to the SlideController.
It builds the environment, starts communcations with the background
OpenOffice task started earlier. If OpenOffice is not present is is
started. Once the environment is available the presentation is loaded
and started.
Called when a presentation is added to the SlideController. It builds the environment, starts communcations with
the background OpenOffice task started earlier. If OpenOffice is not present is is started. Once the environment
is available the presentation is loaded and started.
"""
log.debug(u'Load Presentation OpenOffice')
if os.name == u'nt':
@ -239,13 +234,12 @@ class ImpressDocument(PresentationDocument):
self.desktop = desktop
properties = []
if os.name != u'nt':
# Recent versions of Impress on Windows won't start the presentation
# if it starts as minimized. It seems OK on Linux though.
# Recent versions of Impress on Windows won't start the presentation if it starts as minimized. It seems OK
# on Linux though.
properties.append(self.create_property(u'Minimized', True))
properties = tuple(properties)
try:
self.document = desktop.loadComponentFromURL(url, u'_blank',
0, properties)
self.document = desktop.loadComponentFromURL(url, u'_blank', 0, properties)
except:
log.warn(u'Failed to load presentation %s' % url)
return False
@ -262,33 +256,33 @@ class ImpressDocument(PresentationDocument):
def create_thumbnails(self):
"""
Create thumbnail images for presentation
Create thumbnail images for presentation.
"""
log.debug(u'create thumbnails OpenOffice')
if self.check_thumbnails():
return
if os.name == u'nt':
thumbdirurl = u'file:///' + self.get_temp_folder().replace(u'\\', u'/') \
thumb_dir_url = u'file:///' + self.get_temp_folder().replace(u'\\', u'/') \
.replace(u':', u'|').replace(u' ', u'%20')
else:
thumbdirurl = uno.systemPathToFileUrl(self.get_temp_folder())
props = []
props.append(self.create_property(u'FilterName', u'impress_png_Export'))
props = tuple(props)
thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder())
properties = []
properties.append(self.create_property(u'FilterName', u'impress_png_Export'))
properties = tuple(properties)
doc = self.document
pages = doc.getDrawPages()
if not pages:
return
if not os.path.isdir(self.get_temp_folder()):
os.makedirs(self.get_temp_folder())
for idx in range(pages.getCount()):
page = pages.getByIndex(idx)
for index in range(pages.getCount()):
page = pages.getByIndex(index)
doc.getCurrentController().setCurrentPage(page)
urlpath = u'%s/%s.png' % (thumbdirurl, unicode(idx + 1))
path = os.path.join(self.get_temp_folder(), unicode(idx + 1) + u'.png')
url_path = u'%s/%s.png' % (thumb_dir_url, unicode(index + 1))
path = os.path.join(self.get_temp_folder(), unicode(index + 1) + u'.png')
try:
doc.storeToURL(urlpath, props)
self.convert_thumbnail(path, idx + 1)
doc.storeToURL(url_path, properties)
self.convert_thumbnail(path, index + 1)
delete_file(path)
except ErrorCodeIOException, exception:
log.exception(u'ERROR! ErrorCodeIOException %d' % exception.ErrCode)
@ -297,23 +291,21 @@ class ImpressDocument(PresentationDocument):
def create_property(self, name, value):
"""
Create an OOo style property object which are passed into some
Uno methods
Create an OOo style property object which are passed into some Uno methods.
"""
log.debug(u'create property OpenOffice')
if os.name == u'nt':
prop = self.controller.manager.Bridge_GetStruct(u'com.sun.star.beans.PropertyValue')
property_object = self.controller.manager.Bridge_GetStruct(u'com.sun.star.beans.PropertyValue')
else:
prop = PropertyValue()
prop.Name = name
prop.Value = value
return prop
property_object = PropertyValue()
property_object.Name = name
property_object.Value = value
return property_object
def close_presentation(self):
"""
Close presentation and clean up objects
Triggered by new object being added to SlideController or OpenLP
being shutdown
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
shutdown.
"""
log.debug(u'close Presentation OpenOffice')
if self.document:
@ -329,7 +321,7 @@ class ImpressDocument(PresentationDocument):
def is_loaded(self):
"""
Returns true if a presentation is loaded
Returns true if a presentation is loaded.
"""
log.debug(u'is loaded OpenOffice')
if self.presentation is None or self.document is None:
@ -346,7 +338,7 @@ class ImpressDocument(PresentationDocument):
def is_active(self):
"""
Returns true if a presentation is active and running
Returns true if a presentation is active and running.
"""
log.debug(u'is active OpenOffice')
if not self.is_loaded():
@ -355,21 +347,21 @@ class ImpressDocument(PresentationDocument):
def unblank_screen(self):
"""
Unblanks the screen
Unblanks the screen.
"""
log.debug(u'unblank screen OpenOffice')
return self.control.resume()
def blank_screen(self):
"""
Blanks the screen
Blanks the screen.
"""
log.debug(u'blank screen OpenOffice')
self.control.blankScreen(0)
def is_blank(self):
"""
Returns true if screen is blank
Returns true if screen is blank.
"""
log.debug(u'is blank OpenOffice')
if self.control and self.control.isRunning():
@ -379,7 +371,7 @@ class ImpressDocument(PresentationDocument):
def stop_presentation(self):
"""
Stop the presentation, remove from screen
Stop the presentation, remove from screen.
"""
log.debug(u'stop presentation OpenOffice')
# deactivate should hide the screen according to docs, but doesn't
@ -389,18 +381,17 @@ class ImpressDocument(PresentationDocument):
def start_presentation(self):
"""
Start the presentation from the beginning
Start the presentation from the beginning.
"""
log.debug(u'start presentation OpenOffice')
if self.control is None or not self.control.isRunning():
self.presentation.start()
self.control = self.presentation.getController()
# start() returns before the Component is ready.
# Try for 15 seconds
i = 1
while not self.control and i < 150:
# start() returns before the Component is ready. Try for 15 seconds.
sleep_count = 1
while not self.control and sleep_count < 150:
time.sleep(0.1)
i += 1
sleep_count += 1
self.control = self.presentation.getController()
else:
self.control.activate()
@ -408,25 +399,25 @@ class ImpressDocument(PresentationDocument):
def get_slide_number(self):
"""
Return the current slide number on the screen, from 1
Return the current slide number on the screen, from 1.
"""
return self.control.getCurrentSlideIndex() + 1
def get_slide_count(self):
"""
Return the total number of slides
Return the total number of slides.
"""
return self.document.getDrawPages().getCount()
def goto_slide(self, slideno):
"""
Go to a specific slide (from 1)
Go to a specific slide (from 1).
"""
self.control.gotoSlideIndex(slideno-1)
def next_step(self):
"""
Triggers the next effect of slide on the running presentation
Triggers the next effect of slide on the running presentation.
"""
is_paused = self.control.isPaused()
self.control.gotoNextEffect()
@ -436,7 +427,7 @@ class ImpressDocument(PresentationDocument):
def previous_step(self):
"""
Triggers the previous slide on the running presentation
Triggers the previous slide on the running presentation.
"""
self.control.gotoPreviousSlide()
@ -470,8 +461,8 @@ class ImpressDocument(PresentationDocument):
page = pages.getByIndex(slide_no - 1)
if notes:
page = page.getNotesPage()
for idx in range(page.getCount()):
shape = page.getByIndex(idx)
for index in range(page.getCount()):
shape = page.getByIndex(index)
if shape.supportsService("com.sun.star.drawing.Text"):
text += shape.getString() + '\n'
return text

View File

@ -35,17 +35,20 @@ from PyQt4 import QtCore, QtGui
from openlp.core.lib import MediaManagerItem, Registry, ItemCapabilities, ServiceItemContext, Settings, UiStrings, \
build_icon, check_item_selected, create_thumb, translate, validate_thumb
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
from openlp.core.utils import locale_compare
from openlp.core.utils import get_locale_key
from openlp.plugins.presentations.lib import MessageListener
log = logging.getLogger(__name__)
ERROR = QtGui.QImage(u':/general/general_delete.png')
ERROR_IMAGE = QtGui.QImage(u':/general/general_delete.png')
class PresentationMediaItem(MediaManagerItem):
"""
This is the Presentation media manager item for Presentation Items.
It can present files using Openoffice and Powerpoint
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
Powerpoint
"""
log.info(u'Presentations Media Item loaded')
@ -71,25 +74,25 @@ class PresentationMediaItem(MediaManagerItem):
"""
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
self.Automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
self.displayTypeLabel.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
def build_file_mask_string(self):
"""
Build the list of file extensions to be used in the Open file dialog
Build the list of file extensions to be used in the Open file dialog.
"""
fileType = u''
file_type = u''
for controller in self.controllers:
if self.controllers[controller].enabled():
types = self.controllers[controller].supports + self.controllers[controller].alsosupports
for type in types:
if fileType.find(type) == -1:
fileType += u'*.%s ' % type
self.service_manager.supported_suffixes(type)
self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % fileType
file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
for file_type in file_types:
if file_type.find(file_type) == -1:
file_type += u'*.%s ' % file_type
self.service_manager.supported_suffixes(file_type)
self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % file_type
def required_icons(self):
"""
Set which icons the media manager tab should show
Set which icons the media manager tab should show.
"""
MediaManagerItem.required_icons(self)
self.has_file_icon = True
@ -98,21 +101,21 @@ class PresentationMediaItem(MediaManagerItem):
def add_end_header_bar(self):
"""
Display custom media manager items for presentations
Display custom media manager items for presentations.
"""
self.presentationWidget = QtGui.QWidget(self)
self.presentationWidget.setObjectName(u'presentationWidget')
self.displayLayout = QtGui.QFormLayout(self.presentationWidget)
self.displayLayout.setMargin(self.displayLayout.spacing())
self.displayLayout.setObjectName(u'displayLayout')
self.displayTypeLabel = QtGui.QLabel(self.presentationWidget)
self.displayTypeLabel.setObjectName(u'displayTypeLabel')
self.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.presentationWidget,
u'displayTypeComboBox')
self.displayTypeLabel.setBuddy(self.displayTypeComboBox)
self.displayLayout.addRow(self.displayTypeLabel, self.displayTypeComboBox)
# Add the Presentation widget to the page layout
self.page_layout.addWidget(self.presentationWidget)
self.presentation_widget = QtGui.QWidget(self)
self.presentation_widget.setObjectName(u'presentation_widget')
self.display_layout = QtGui.QFormLayout(self.presentation_widget)
self.display_layout.setMargin(self.display_layout.spacing())
self.display_layout.setObjectName(u'display_layout')
self.display_type_label = QtGui.QLabel(self.presentation_widget)
self.display_type_label.setObjectName(u'display_type_label')
self.display_type_combo_box = create_horizontal_adjusting_combo_box(self.presentation_widget,
u'display_type_combo_box')
self.display_type_label.setBuddy(self.display_type_combo_box)
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
# Add the Presentation widget to the page layout.
self.page_layout.addWidget(self.presentation_widget)
def initialise(self):
"""
@ -120,56 +123,54 @@ class PresentationMediaItem(MediaManagerItem):
"""
self.list_view.setIconSize(QtCore.QSize(88, 50))
files = Settings().value(self.settings_section + u'/presentations files')
self.load_list(files, initialLoad=True)
self.load_list(files, initial_load=True)
self.populate_display_types()
def populate_display_types(self):
"""
Load the combobox with the enabled presentation controllers,
allowing user to select a specific app if settings allow
Load the combobox with the enabled presentation controllers, allowing user to select a specific app if settings
allow.
"""
self.displayTypeComboBox.clear()
self.display_type_combo_box.clear()
for item in self.controllers:
# load the drop down selection
if self.controllers[item].enabled():
self.displayTypeComboBox.addItem(item)
if self.displayTypeComboBox.count() > 1:
self.displayTypeComboBox.insertItem(0, self.Automatic)
self.displayTypeComboBox.setCurrentIndex(0)
self.display_type_combo_box.addItem(item)
if self.display_type_combo_box.count() > 1:
self.display_type_combo_box.insertItem(0, self.Automatic)
self.display_type_combo_box.setCurrentIndex(0)
if Settings().value(self.settings_section + u'/override app') == QtCore.Qt.Checked:
self.presentationWidget.show()
self.presentation_widget.show()
else:
self.presentationWidget.hide()
self.presentation_widget.hide()
def load_list(self, files, target_group=None, initialLoad=False):
def load_list(self, files, target_group=None, initial_load=False):
"""
Add presentations into the media manager
This is called both on initial load of the plugin to populate with
existing files, and when the user adds new files via the media manager
Add presentations into the media manager. This is called both on initial load of the plugin to populate with
existing files, and when the user adds new files via the media manager.
"""
currlist = self.get_file_list()
titles = [os.path.split(file)[1] for file in currlist]
current_list = self.get_file_list()
titles = [os.path.split(file)[1] for file in current_list]
self.application.set_busy_cursor()
if not initialLoad:
if not initial_load:
self.main_window.display_progress_bar(len(files))
# Sort the presentations by its filename considering language specific characters.
files.sort(cmp=locale_compare,
key=lambda filename: os.path.split(unicode(filename))[1])
files.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
for file in files:
if not initialLoad:
if not initial_load:
self.main_window.increment_progress_bar()
if currlist.count(file) > 0:
if current_list.count(file) > 0:
continue
filename = os.path.split(unicode(file))[1]
if not os.path.exists(file):
item_name = QtGui.QListWidgetItem(filename)
item_name.setIcon(build_icon(ERROR))
item_name.setIcon(build_icon(ERROR_IMAGE))
item_name.setData(QtCore.Qt.UserRole, file)
item_name.setToolTip(file)
self.list_view.addItem(item_name)
else:
if titles.count(filename) > 0:
if not initialLoad:
if not initial_load:
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
translate('PresentationPlugin.MediaItem',
'A presentation with that filename already exists.')
@ -181,7 +182,7 @@ class PresentationMediaItem(MediaManagerItem):
doc = controller.add_document(unicode(file))
thumb = os.path.join(doc.get_thumbnail_folder(), u'icon.png')
preview = doc.get_thumbnail_path(1, True)
if not preview and not initialLoad:
if not preview and not initial_load:
doc.load_presentation()
preview = doc.get_thumbnail_path(1, True)
doc.close_presentation()
@ -193,7 +194,7 @@ class PresentationMediaItem(MediaManagerItem):
else:
icon = create_thumb(preview, thumb)
else:
if initialLoad:
if initial_load:
icon = build_icon(u':/general/general_delete.png')
else:
critical_error_message_box(UiStrings().UnsupportedFile,
@ -204,13 +205,13 @@ class PresentationMediaItem(MediaManagerItem):
item_name.setIcon(icon)
item_name.setToolTip(file)
self.list_view.addItem(item_name)
if not initialLoad:
if not initial_load:
self.main_window.finished_progress_bar()
self.application.set_normal_cursor()
def on_delete_click(self):
"""
Remove a presentation item from the list
Remove a presentation item from the list.
"""
if check_item_selected(self.list_view, UiStrings().SelectDelete):
items = self.list_view.selectedIndexes()
@ -231,12 +232,11 @@ class PresentationMediaItem(MediaManagerItem):
self.list_view.takeItem(row)
Settings().setValue(self.settings_section + u'/presentations files', self.get_file_list())
def generate_slide_data(self, service_item, item=None, xmlVersion=False,
def generate_slide_data(self, service_item, item=None, xml_version=False,
remote=False, context=ServiceItemContext.Service):
"""
Load the relevant information for displaying the presentation
in the slidecontroller. In the case of powerpoints, an image
for each slide
Load the relevant information for displaying the presentation in the slidecontroller. In the case of
powerpoints, an image for each slide.
"""
if item:
items = [item]
@ -244,8 +244,8 @@ class PresentationMediaItem(MediaManagerItem):
items = self.list_view.selectedItems()
if len(items) > 1:
return False
service_item.title = self.displayTypeComboBox.currentText()
service_item.shortname = self.displayTypeComboBox.currentText()
service_item.title = self.display_type_combo_box.currentText()
service_item.shortname = self.display_type_combo_box.currentText()
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay)
shortname = service_item.shortname
@ -288,26 +288,24 @@ class PresentationMediaItem(MediaManagerItem):
def findControllerByType(self, filename):
"""
Determine the default application controller to use for the selected
file type. This is used if "Automatic" is set as the preferred
controller. Find the first (alphabetic) enabled controller which
"supports" the extension. If none found, then look for a controller
which "also supports" it instead.
Determine the default application controller to use for the selected file type. This is used if "Automatic" is
set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
If none found, then look for a controller which "also supports" it instead.
"""
filetype = os.path.splitext(filename)[1][1:]
if not filetype:
file_type = os.path.splitext(filename)[1][1:]
if not file_type:
return None
for controller in self.controllers:
if self.controllers[controller].enabled():
if filetype in self.controllers[controller].supports:
if file_type in self.controllers[controller].supports:
return controller
for controller in self.controllers:
if self.controllers[controller].enabled():
if filetype in self.controllers[controller].alsosupports:
if file_type in self.controllers[controller].also_supports:
return controller
return None
def search(self, string, showError):
def search(self, string, show_error):
files = Settings().value(self.settings_section + u'/presentations files')
results = []
string = string.lower()

View File

@ -38,8 +38,8 @@ log = logging.getLogger(__name__)
class Controller(object):
"""
This is the Presentation listener who acts on events from the slide
controller and passes the messages on the the correct presentation handlers
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
correct presentation handlers.
"""
log.info(u'Controller loaded')
@ -54,9 +54,8 @@ class Controller(object):
def add_handler(self, controller, file, hide_mode, slide_no):
"""
Add a handler, which is an instance of a presentation and
slidecontroller combination. If the slidecontroller has a display
then load the presentation.
Add a handler, which is an instance of a presentation and slidecontroller combination. If the slidecontroller
has a display then load the presentation.
"""
log.debug(u'Live = %s, add_handler %s' % (self.is_live, file))
self.controller = controller
@ -86,8 +85,7 @@ class Controller(object):
def activate(self):
"""
Active the presentation, and show it on the screen.
Use the last slide number.
Active the presentation, and show it on the screen. Use the last slide number.
"""
log.debug(u'Live = %s, activate' % self.is_live)
if not self.doc:
@ -130,7 +128,7 @@ class Controller(object):
def first(self):
"""
Based on the handler passed at startup triggers the first slide
Based on the handler passed at startup triggers the first slide.
"""
log.debug(u'Live = %s, first' % self.is_live)
if not self.doc:
@ -148,7 +146,7 @@ class Controller(object):
def last(self):
"""
Based on the handler passed at startup triggers the last slide
Based on the handler passed at startup triggers the last slide.
"""
log.debug(u'Live = %s, last' % self.is_live)
if not self.doc:
@ -166,7 +164,7 @@ class Controller(object):
def next(self):
"""
Based on the handler passed at startup triggers the next slide event
Based on the handler passed at startup triggers the next slide event.
"""
log.debug(u'Live = %s, next' % self.is_live)
if not self.doc:
@ -182,9 +180,8 @@ class Controller(object):
return
if not self.activate():
return
# The "End of slideshow" screen is after the last slide
# Note, we can't just stop on the last slide, since it may
# contain animations that need to be stepped through.
# The "End of slideshow" screen is after the last slide. Note, we can't just stop on the last slide, since it
# may contain animations that need to be stepped through.
if self.doc.slidenumber > self.doc.get_slide_count():
return
self.doc.next_step()
@ -192,7 +189,7 @@ class Controller(object):
def previous(self):
"""
Based on the handler passed at startup triggers the previous slide event
Based on the handler passed at startup triggers the previous slide event.
"""
log.debug(u'Live = %s, previous' % self.is_live)
if not self.doc:
@ -213,7 +210,7 @@ class Controller(object):
def shutdown(self):
"""
Based on the handler passed at startup triggers slide show to shut down
Based on the handler passed at startup triggers slide show to shut down.
"""
log.debug(u'Live = %s, shutdown' % self.is_live)
if not self.doc:
@ -223,7 +220,7 @@ class Controller(object):
def blank(self, hide_mode):
"""
Instruct the controller to blank the presentation
Instruct the controller to blank the presentation.
"""
log.debug(u'Live = %s, blank' % self.is_live)
self.hide_mode = hide_mode
@ -244,7 +241,7 @@ class Controller(object):
def stop(self):
"""
Instruct the controller to stop and hide the presentation
Instruct the controller to stop and hide the presentation.
"""
log.debug(u'Live = %s, stop' % self.is_live)
self.hide_mode = HideMode.Screen
@ -260,7 +257,7 @@ class Controller(object):
def unblank(self):
"""
Instruct the controller to unblank the presentation
Instruct the controller to unblank the presentation.
"""
log.debug(u'Live = %s, unblank' % self.is_live)
self.hide_mode = None
@ -283,8 +280,8 @@ class Controller(object):
class MessageListener(object):
"""
This is the Presentation listener who acts on events from the slide
controller and passes the messages on the the correct presentation handlers
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
correct presentation handlers
"""
log.info(u'Message Listener loaded')
@ -310,12 +307,11 @@ class MessageListener(object):
def startup(self, message):
"""
Start of new presentation
Save the handler as any new presentations start here
Start of new presentation. Save the handler as any new presentations start here
"""
log.debug(u'Startup called with message %s' % message)
is_live = message[1]
item = message[0]
log.debug(u'Startup called with message %s' % message)
hide_mode = message[2]
file = item.get_frame_path()
self.handler = item.title
@ -331,7 +327,7 @@ class MessageListener(object):
def slide(self, message):
"""
React to the message to move to a specific slide
React to the message to move to a specific slide.
"""
is_live = message[1]
slide = message[2]
@ -342,7 +338,7 @@ class MessageListener(object):
def first(self, message):
"""
React to the message to move to the first slide
React to the message to move to the first slide.
"""
is_live = message[1]
if is_live:
@ -352,7 +348,7 @@ class MessageListener(object):
def last(self, message):
"""
React to the message to move to the last slide
React to the message to move to the last slide.
"""
is_live = message[1]
if is_live:
@ -362,7 +358,7 @@ class MessageListener(object):
def next(self, message):
"""
React to the message to move to the next animation/slide
React to the message to move to the next animation/slide.
"""
is_live = message[1]
if is_live:
@ -372,7 +368,7 @@ class MessageListener(object):
def previous(self, message):
"""
React to the message to move to the previous animation/slide
React to the message to move to the previous animation/slide.
"""
is_live = message[1]
if is_live:
@ -382,8 +378,7 @@ class MessageListener(object):
def shutdown(self, message):
"""
React to message to shutdown the presentation. I.e. end the show
and close the file
React to message to shutdown the presentation. I.e. end the show and close the file.
"""
is_live = message[1]
if is_live:
@ -393,7 +388,7 @@ class MessageListener(object):
def hide(self, message):
"""
React to the message to show the desktop
React to the message to show the desktop.
"""
is_live = message[1]
if is_live:
@ -401,7 +396,7 @@ class MessageListener(object):
def blank(self, message):
"""
React to the message to blank the display
React to the message to blank the display.
"""
is_live = message[1]
hide_mode = message[2]
@ -410,7 +405,7 @@ class MessageListener(object):
def unblank(self, message):
"""
React to the message to unblank the display
React to the message to unblank the display.
"""
is_live = message[1]
if is_live:
@ -418,9 +413,7 @@ class MessageListener(object):
def timeout(self):
"""
The presentation may be timed or might be controlled by the
application directly, rather than through OpenLP. Poll occasionally
to check which slide is currently displayed so the slidecontroller
view can be updated
The presentation may be timed or might be controlled by the application directly, rather than through OpenLP.
Poll occasionally to check which slide is currently displayed so the slidecontroller view can be updated.
"""
self.live_handler.poll()

View File

@ -26,7 +26,10 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This modul is for controlling powerpiont. PPT API documentation:
`http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
"""
import os
import logging
@ -39,16 +42,14 @@ if os.name == u'nt':
from openlp.core.lib import ScreenList
from presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)
# PPT API documentation:
# http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx
class PowerpointController(PresentationController):
"""
Class to control interactions with PowerPoint Presentations
It creates the runtime Environment , Loads the and Closes the Presentation
As well as triggering the correct activities based on the users input
Class to control interactions with PowerPoint Presentations. It creates the runtime Environment , Loads the and
Closes the Presentation. As well as triggering the correct activities based on the users input.
"""
log.info(u'PowerpointController loaded')
@ -63,7 +64,7 @@ class PowerpointController(PresentationController):
def check_available(self):
"""
PowerPoint is able to run on this machine
PowerPoint is able to run on this machine.
"""
log.debug(u'check_available')
if os.name == u'nt':
@ -77,7 +78,7 @@ class PowerpointController(PresentationController):
if os.name == u'nt':
def start_process(self):
"""
Loads PowerPoint process
Loads PowerPoint process.
"""
log.debug(u'start_process')
if not self.process:
@ -87,7 +88,7 @@ class PowerpointController(PresentationController):
def kill(self):
"""
Called at system exit to clean up any running presentations
Called at system exit to clean up any running presentations.
"""
log.debug(u'Kill powerpoint')
while self.docs:
@ -105,12 +106,12 @@ class PowerpointController(PresentationController):
class PowerpointDocument(PresentationDocument):
"""
Class which holds information and controls a single presentation
Class which holds information and controls a single presentation.
"""
def __init__(self, controller, presentation):
"""
Constructor, store information about the file and initialise
Constructor, store information about the file and initialise.
"""
log.debug(u'Init Presentation Powerpoint')
PresentationDocument.__init__(self, controller, presentation)
@ -118,8 +119,8 @@ class PowerpointDocument(PresentationDocument):
def load_presentation(self):
"""
Called when a presentation is added to the SlideController.
Opens the PowerPoint file using the process created earlier.
Called when a presentation is added to the SlideController. Opens the PowerPoint file using the process created
earlier.
"""
log.debug(u'load_presentation')
if not self.controller.process or not self.controller.process.Visible:
@ -142,20 +143,19 @@ class PowerpointDocument(PresentationDocument):
self.presentation.Slides[n].Copy()
thumbnail = QApplication.clipboard.image()
However, for the moment, we want a physical file since it makes life
easier elsewhere.
However, for the moment, we want a physical file since it makes life easier elsewhere.
"""
log.debug(u'create_thumbnails')
if self.check_thumbnails():
return
for num in range(self.presentation.Slides.Count):
self.presentation.Slides(num + 1).Export(os.path.join(
self.get_thumbnail_folder(), 'slide%d.png' % (num + 1)), 'png', 320, 240)
self.presentation.Slides(num + 1).Export(
os.path.join(self.get_thumbnail_folder(), 'slide%d.png' % (num + 1)), 'png', 320, 240)
def close_presentation(self):
"""
Close presentation and clean up objects. This is triggered by a new
object being added to SlideController or OpenLP being shut down.
Close presentation and clean up objects. This is triggered by a new object being added to SlideController or
OpenLP being shut down.
"""
log.debug(u'ClosePresentation')
if self.presentation:
@ -182,7 +182,6 @@ class PowerpointDocument(PresentationDocument):
return False
return True
def is_active(self):
"""
Returns ``True`` if a presentation is currently active.
@ -253,15 +252,14 @@ class PowerpointDocument(PresentationDocument):
dpi = win32ui.GetForegroundWindow().GetDC().GetDeviceCaps(88)
except win32ui.error:
dpi = 96
rect = ScreenList().current[u'size']
size = ScreenList().current[u'size']
ppt_window = self.presentation.SlideShowSettings.Run()
if not ppt_window:
return
ppt_window.Top = rect.y() * 72 / dpi
ppt_window.Height = rect.height() * 72 / dpi
ppt_window.Left = rect.x() * 72 / dpi
ppt_window.Width = rect.width() * 72 / dpi
ppt_window.Top = size.y() * 72 / dpi
ppt_window.Height = size.height() * 72 / dpi
ppt_window.Left = size.x() * 72 / dpi
ppt_window.Width = size.width() * 72 / dpi
def get_slide_number(self):
"""
@ -318,6 +316,7 @@ class PowerpointDocument(PresentationDocument):
"""
return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
def _get_text_from_shapes(shapes):
"""
Returns any text extracted from the shapes on a presentation slide.
@ -326,8 +325,8 @@ def _get_text_from_shapes(shapes):
A set of shapes to search for text.
"""
text = ''
for idx in range(shapes.Count):
shape = shapes(idx + 1)
for index in range(shapes.Count):
shape = shapes(index + 1)
if shape.HasTextFrame:
text += shape.TextFrame.TextRange.Text + '\n'
return text

View File

@ -37,13 +37,14 @@ if os.name == u'nt':
from openlp.core.lib import ScreenList
from presentationcontroller import PresentationController, PresentationDocument
log = logging.getLogger(__name__)
class PptviewController(PresentationController):
"""
Class to control interactions with PowerPoint Viewer Presentations
It creates the runtime Environment , Loads the and Closes the Presentation
As well as triggering the correct activities based on the users input
Class to control interactions with PowerPoint Viewer Presentations. It creates the runtime Environment , Loads the
and Closes the Presentation. As well as triggering the correct activities based on the users input
"""
log.info(u'PPTViewController loaded')
@ -58,7 +59,7 @@ class PptviewController(PresentationController):
def check_available(self):
"""
PPT Viewer is able to run on this machine
PPT Viewer is able to run on this machine.
"""
log.debug(u'check_available')
if os.name != u'nt':
@ -68,7 +69,7 @@ class PptviewController(PresentationController):
if os.name == u'nt':
def check_installed(self):
"""
Check the viewer is installed
Check the viewer is installed.
"""
log.debug(u'Check installed')
try:
@ -79,14 +80,14 @@ class PptviewController(PresentationController):
def start_process(self):
"""
Loads the PPTVIEWLIB library
Loads the PPTVIEWLIB library.
"""
if self.process:
return
log.debug(u'start PPTView')
dllpath = os.path.join(self.plugin_manager.base_path, u'presentations', u'lib', u'pptviewlib',
u'pptviewlib.dll')
self.process = cdll.LoadLibrary(dllpath)
dll_path = os.path.join(
self.plugin_manager.base_path, u'presentations', u'lib', u'pptviewlib', u'pptviewlib.dll')
self.process = cdll.LoadLibrary(dll_path)
if log.isEnabledFor(logging.DEBUG):
self.process.SetDebug(1)
@ -101,33 +102,32 @@ class PptviewController(PresentationController):
class PptviewDocument(PresentationDocument):
"""
Class which holds information and controls a single presentation
Class which holds information and controls a single presentation.
"""
def __init__(self, controller, presentation):
"""
Constructor, store information about the file and initialise
Constructor, store information about the file and initialise.
"""
log.debug(u'Init Presentation PowerPoint')
PresentationDocument.__init__(self, controller, presentation)
self.presentation = None
self.pptid = None
self.ppt_id = None
self.blanked = False
self.hidden = False
def load_presentation(self):
"""
Called when a presentation is added to the SlideController.
It builds the environment, starts communication with the background
PptView task started earlier.
Called when a presentation is added to the SlideController. It builds the environment, starts communication with
the background PptView task started earlier.
"""
log.debug(u'LoadPresentation')
rect = ScreenList().current[u'size']
rect = RECT(rect.x(), rect.y(), rect.right(), rect.bottom())
size = ScreenList().current[u'size']
rect = RECT(size.x(), size.y(), size.right(), size.bottom())
filepath = str(self.filepath.replace(u'/', u'\\'))
if not os.path.isdir(self.get_temp_folder()):
os.makedirs(self.get_temp_folder())
self.pptid = self.controller.process.OpenPPT(filepath, None, rect, str(self.get_temp_folder()) + '\\slide')
if self.pptid >= 0:
self.ppt_id = self.controller.process.OpenPPT(filepath, None, rect, str(self.get_temp_folder()) + '\\slide')
if self.ppt_id >= 0:
self.create_thumbnails()
self.stop_presentation()
return True
@ -136,8 +136,7 @@ class PptviewDocument(PresentationDocument):
def create_thumbnails(self):
"""
PPTviewLib creates large BMP's, but we want small PNG's for consistency.
Convert them here.
PPTviewLib creates large BMP's, but we want small PNG's for consistency. Convert them here.
"""
log.debug(u'create_thumbnails')
if self.check_thumbnails():
@ -149,21 +148,20 @@ class PptviewDocument(PresentationDocument):
def close_presentation(self):
"""
Close presentation and clean up objects
Triggered by new object being added to SlideController orOpenLP
being shut down
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
shut down.
"""
log.debug(u'ClosePresentation')
if self.controller.process:
self.controller.process.ClosePPT(self.pptid)
self.pptid = -1
self.controller.process.ClosePPT(self.ppt_id)
self.ppt_id = -1
self.controller.remove_doc(self)
def is_loaded(self):
"""
Returns true if a presentation is loaded
Returns true if a presentation is loaded.
"""
if self.pptid < 0:
if self.ppt_id < 0:
return False
if self.get_slide_count() < 0:
return False
@ -171,74 +169,74 @@ class PptviewDocument(PresentationDocument):
def is_active(self):
"""
Returns true if a presentation is currently active
Returns true if a presentation is currently active.
"""
return self.is_loaded() and not self.hidden
def blank_screen(self):
"""
Blanks the screen
Blanks the screen.
"""
self.controller.process.Blank(self.pptid)
self.controller.process.Blank(self.ppt_id)
self.blanked = True
def unblank_screen(self):
"""
Unblanks (restores) the presentation
Unblanks (restores) the presentation.
"""
self.controller.process.Unblank(self.pptid)
self.controller.process.Unblank(self.ppt_id)
self.blanked = False
def is_blank(self):
"""
Returns true if screen is blank
Returns true if screen is blank.
"""
log.debug(u'is blank OpenOffice')
return self.blanked
def stop_presentation(self):
"""
Stops the current presentation and hides the output
Stops the current presentation and hides the output.
"""
self.hidden = True
self.controller.process.Stop(self.pptid)
self.controller.process.Stop(self.ppt_id)
def start_presentation(self):
"""
Starts a presentation from the beginning
Starts a presentation from the beginning.
"""
if self.hidden:
self.hidden = False
self.controller.process.Resume(self.pptid)
self.controller.process.Resume(self.ppt_id)
else:
self.controller.process.RestartShow(self.pptid)
self.controller.process.RestartShow(self.ppt_id)
def get_slide_number(self):
"""
Returns the current slide number
Returns the current slide number.
"""
return self.controller.process.GetCurrentSlide(self.pptid)
return self.controller.process.GetCurrentSlide(self.ppt_id)
def get_slide_count(self):
"""
Returns total number of slides
Returns total number of slides.
"""
return self.controller.process.GetSlideCount(self.pptid)
return self.controller.process.GetSlideCount(self.ppt_id)
def goto_slide(self, slideno):
"""
Moves to a specific slide in the presentation
Moves to a specific slide in the presentation.
"""
self.controller.process.GotoSlide(self.pptid, slideno)
self.controller.process.GotoSlide(self.ppt_id, slideno)
def next_step(self):
"""
Triggers the next effect of slide on the running presentation
Triggers the next effect of slide on the running presentation.
"""
self.controller.process.NextStep(self.pptid)
self.controller.process.NextStep(self.ppt_id)
def previous_step(self):
"""
Triggers the previous slide on the running presentation
Triggers the previous slide on the running presentation.
"""
self.controller.process.PrevStep(self.pptid)
self.controller.process.PrevStep(self.ppt_id)

View File

@ -40,9 +40,8 @@ log = logging.getLogger(__name__)
class PresentationDocument(object):
"""
Base class for presentation documents to inherit from.
Loads and closes the presentation as well as triggering the correct
activities based on the users input
Base class for presentation documents to inherit from. Loads and closes the presentation as well as triggering the
correct activities based on the users input
**Hook Functions**
@ -131,20 +130,17 @@ class PresentationDocument(object):
"""
The location where thumbnail images will be stored
"""
return os.path.join(
self.controller.thumbnail_folder, self.get_file_name())
return os.path.join(self.controller.thumbnail_folder, self.get_file_name())
def get_temp_folder(self):
"""
The location where thumbnail images will be stored
"""
return os.path.join(
self.controller.temp_folder, self.get_file_name())
return os.path.join(self.controller.temp_folder, self.get_file_name())
def check_thumbnails(self):
"""
Returns ``True`` if the thumbnail images exist and are more recent than
the powerpoint file.
Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file.
"""
lastimage = self.get_thumbnail_path(self.get_slide_count(), True)
if not (lastimage and os.path.isfile(lastimage)):
@ -153,8 +149,7 @@ class PresentationDocument(object):
def close_presentation(self):
"""
Close presentation and clean up objects
Triggered by new object being added to SlideController
Close presentation and clean up objects. Triggered by new object being added to SlideController
"""
self.controller.close_presentation()
@ -223,8 +218,8 @@ class PresentationDocument(object):
def next_step(self):
"""
Triggers the next effect of slide on the running presentation
This might be the next animation on the current slide, or the next slide
Triggers the next effect of slide on the running presentation. This might be the next animation on the current
slide, or the next slide
"""
pass
@ -236,8 +231,7 @@ class PresentationDocument(object):
def convert_thumbnail(self, file, idx):
"""
Convert the slide image the application made to a standard 320x240
.png image.
Convert the slide image the application made to a standard 320x240 .png image.
"""
if self.check_thumbnails():
return
@ -281,7 +275,7 @@ class PresentationDocument(object):
Returns the text on the slide
``slide_no``
The slide the text is required for, starting at 1
The slide the text is required for, starting at 1
"""
return ''
@ -290,24 +284,21 @@ class PresentationDocument(object):
Returns the text on the slide
``slide_no``
The slide the notes are required for, starting at 1
The slide the notes are required for, starting at 1
"""
return ''
class PresentationController(object):
"""
This class is used to control interactions with presentation applications
by creating a runtime environment. This is a base class for presentation
controllers to inherit from.
This class is used to control interactions with presentation applications by creating a runtime environment. This is
a base class for presentation controllers to inherit from.
To create a new controller, take a copy of this file and name it so it ends
with ``controller.py``, i.e. ``foobarcontroller.py``. Make sure it inherits
:class:`~openlp.plugins.presentations.lib.presentationcontroller.PresentationController`,
and then fill in the blanks. If possible try to make sure it loads on all
platforms, usually by using :mod:``os.name`` checks, although
``__init__``, ``check_available`` and ``presentation_deleted`` should
always be implemented.
To create a new controller, take a copy of this file and name it so it ends with ``controller.py``, i.e.
``foobarcontroller.py``. Make sure it inherits
:class:`~openlp.plugins.presentations.lib.presentationcontroller.PresentationController`, and then fill in the
blanks. If possible try to make sure it loads on all platforms, usually by using :mod:``os.name`` checks, although
``__init__``, ``check_available`` and ``presentation_deleted`` should always be implemented.
See :class:`~openlp.plugins.presentations.lib.impresscontroller.ImpressController`,
:class:`~openlp.plugins.presentations.lib.powerpointcontroller.PowerpointController` or
@ -317,36 +308,34 @@ class PresentationController(object):
**Basic Attributes**
``name``
The name that appears in the options and the media manager
The name that appears in the options and the media manager.
``enabled``
The controller is enabled
The controller is enabled.
``available``
The controller is available on this machine. Set by init via
call to check_available
The controller is available on this machine. Set by init via call to check_available.
``plugin``
The presentationplugin object
The presentationplugin object.
``supports``
The primary native file types this application supports
The primary native file types this application supports.
``alsosupports``
Other file types the application can import, although not necessarily
the first choice due to potential incompatibilities
Other file types the application can import, although not necessarily the first choice due to potential
incompatibilities.
**Hook Functions**
``kill()``
Called at system exit to clean up any running presentations
Called at system exit to clean up any running presentations.
``check_available()``
Returns True if presentation application is installed/can run on this
machine
Returns True if presentation application is installed/can run on this machine.
``presentation_deleted()``
Deletes presentation specific files, e.g. thumbnails
Deletes presentation specific files, e.g. thumbnails.
"""
log.info(u'PresentationController loaded')
@ -354,9 +343,8 @@ class PresentationController(object):
def __init__(self, plugin=None, name=u'PresentationController',
document_class=PresentationDocument):
"""
This is the constructor for the presentationcontroller object. This
provides an easy way for descendent plugins to populate common data.
This method *must* be overridden, like so::
This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
to populate common data. This method *must* be overridden, like so::
class MyPresentationController(PresentationController):
def __init__(self, plugin):
@ -399,28 +387,26 @@ class PresentationController(object):
def check_available(self):
"""
Presentation app is able to run on this machine
Presentation app is able to run on this machine.
"""
return False
def start_process(self):
"""
Loads a running version of the presentation application in the
background.
Loads a running version of the presentation application in the background.
"""
pass
def kill(self):
"""
Called at system exit to clean up any running presentations and
close the application
Called at system exit to clean up any running presentations and close the application.
"""
log.debug(u'Kill')
self.close_presentation()
def add_document(self, name):
"""
Called when a new presentation document is opened
Called when a new presentation document is opened.
"""
document = self.document_class(self, name)
self.docs.append(document)
@ -428,7 +414,7 @@ class PresentationController(object):
def remove_doc(self, doc=None):
"""
Called to remove an open document from the collection
Called to remove an open document from the collection.
"""
log.debug(u'remove_doc Presentation')
if doc is None:

View File

@ -91,8 +91,7 @@ class PresentationTab(SettingsTab):
if checkbox.isEnabled():
checkbox.setText(controller.name)
else:
checkbox.setText(
translate('PresentationPlugin.PresentationTab', '%s (unavailable)') % controller.name)
checkbox.setText(translate('PresentationPlugin.PresentationTab', '%s (unavailable)') % controller.name)
def load(self):
"""
@ -106,8 +105,8 @@ class PresentationTab(SettingsTab):
def save(self):
"""
Save the settings. If the tab hasn't been made visible to the user then there is nothing to do,
so exit. This removes the need to start presentation applications unnecessarily.
Save the settings. If the tab hasn't been made visible to the user then there is nothing to do, so exit. This
removes the need to start presentation applications unnecessarily.
"""
if not self.activated:
return

View File

@ -27,8 +27,8 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`presentationplugin` module provides the ability for OpenLP to display
presentations from a variety of document formats.
The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
formats.
"""
import os
import logging
@ -39,8 +39,10 @@ from openlp.core.lib import Plugin, StringContent, build_icon, translate
from openlp.core.utils import AppLocation
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
log = logging.getLogger(__name__)
__default_settings__ = {
u'presentations/override app': QtCore.Qt.Unchecked,
u'presentations/Impress': QtCore.Qt.Checked,
@ -52,9 +54,8 @@ __default_settings__ = {
class PresentationPlugin(Plugin):
"""
This plugin allowed a Presentation to be opened, controlled and displayed
on the output display. The plugin controls third party applications such
as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer
This plugin allowed a Presentation to be opened, controlled and displayed on the output display. The plugin controls
third party applications such as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer.
"""
log = logging.getLogger(u'PresentationPlugin')
@ -69,18 +70,16 @@ class PresentationPlugin(Plugin):
self.icon_path = u':/plugins/plugin_presentations.png'
self.icon = build_icon(self.icon_path)
def create_settings_Tab(self, parent):
def create_settings_tab(self, parent):
"""
Create the settings Tab
Create the settings Tab.
"""
visible_name = self.get_string(StringContent.VisibleName)
self.settings_tab = PresentationTab(parent, self.name, visible_name[u'title'], self.controllers,
self.icon_path)
self.settings_tab = PresentationTab(parent, self.name, visible_name[u'title'], self.controllers, self.icon_path)
def initialise(self):
"""
Initialise the plugin. Determine which controllers are enabled
are start their processes.
Initialise the plugin. Determine which controllers are enabled are start their processes.
"""
log.info(u'Presentations Initialising')
Plugin.initialise(self)
@ -95,8 +94,8 @@ class PresentationPlugin(Plugin):
def finalise(self):
"""
Finalise the plugin. Ask all the enabled presentation applications
to close down their applications and release resources.
Finalise the plugin. Ask all the enabled presentation applications to close down their applications and release
resources.
"""
log.info(u'Plugin Finalise')
# Ask each controller to tidy up.
@ -108,26 +107,23 @@ class PresentationPlugin(Plugin):
def create_media_manager_item(self):
"""
Create the Media Manager List
Create the Media Manager List.
"""
self.media_item = PresentationMediaItem(
self.main_window.media_dock_manager.media_dock, self, self.icon, self.controllers)
def register_controllers(self, controller):
"""
Register each presentation controller (Impress, PPT etc) and store for later use
Register each presentation controller (Impress, PPT etc) and store for later use.
"""
self.controllers[controller.name] = controller
def check_pre_conditions(self):
"""
Check to see if we have any presentation software available
If Not do not install the plugin.
Check to see if we have any presentation software available. If not do not install the plugin.
"""
log.debug(u'check_pre_conditions')
controller_dir = os.path.join(
AppLocation.get_directory(AppLocation.PluginsDir),
u'presentations', u'lib')
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'presentations', u'lib')
for filename in os.listdir(controller_dir):
if filename.endswith(u'controller.py') and not filename == 'presentationcontroller.py':
path = os.path.join(controller_dir, filename)
@ -146,7 +142,7 @@ class PresentationPlugin(Plugin):
def about(self):
"""
Return information about this plugin
Return information about this plugin.
"""
about_text = translate('PresentationPlugin', '<strong>Presentation '
'Plugin</strong><br />The presentation plugin provides the '
@ -157,7 +153,7 @@ class PresentationPlugin(Plugin):
def set_plugin_text_strings(self):
"""
Called to define all translatable texts of the plugin
Called to define all translatable texts of the plugin.
"""
## Name PluginList ##
self.text_strings[StringContent.Name] = {

View File

@ -147,7 +147,7 @@ window.OpenLP = {
},
pollServer: function () {
$.getJSON(
"/api/poll",
"/stage/api/poll",
function (data, status) {
var prevItem = OpenLP.currentItem;
OpenLP.currentSlide = data.results.slide;

View File

@ -26,7 +26,7 @@
window.OpenLP = {
loadService: function (event) {
$.getJSON(
"/api/service/list",
"/stage/api/service/list",
function (data, status) {
OpenLP.nextSong = "";
$("#notes").html("");
@ -46,7 +46,7 @@ window.OpenLP = {
},
loadSlides: function (event) {
$.getJSON(
"/api/controller/live/text",
"/stage/api/controller/live/text",
function (data, status) {
OpenLP.currentSlides = data.results.slides;
OpenLP.currentSlide = 0;
@ -137,7 +137,7 @@ window.OpenLP = {
},
pollServer: function () {
$.getJSON(
"/api/poll",
"/stage/api/poll",
function (data, status) {
OpenLP.updateClock(data);
if (OpenLP.currentItem != data.results.item ||

View File

@ -43,7 +43,7 @@ the remotes.
``/files/{filename}``
Serve a static file.
``/api/poll``
``/stage/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred::
@ -119,122 +119,198 @@ import os
import re
import urllib
import urlparse
import cherrypy
from PyQt4 import QtCore, QtNetwork
from mako.template import Template
from PyQt4 import QtCore
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
from openlp.core.utils import AppLocation, translate
from cherrypy._cpcompat import sha, ntob
log = logging.getLogger(__name__)
class HttpResponse(object):
def make_sha_hash(password):
"""
A simple object to encapsulate a pseudo-http response.
Create an encrypted password for the given password.
"""
code = '200 OK'
content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
return sha(ntob(password)).hexdigest()
def __init__(self, content='', headers=None, code=None):
if headers is None:
headers = {}
self.content = content
for key, value in headers.iteritems():
self.headers[key] = value
if code:
self.code = code
def fetch_password(username):
"""
Fetch the password for a provided user.
"""
if username != Settings().value(u'remotes/user id'):
return None
return make_sha_hash(Settings().value(u'remotes/password'))
class HttpServer(object):
"""
Ability to control OpenLP via a web browser.
This class controls the Cherrypy server and configuration.
"""
def __init__(self, plugin):
_cp_config = {
'tools.sessions.on': True,
'tools.auth.on': True
}
def __init__(self):
"""
Initialise the httpserver, and start the server.
Initialise the http server, and start the server.
"""
log.debug(u'Initialise httpserver')
self.plugin = plugin
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
self.connections = []
self.start_tcp()
self.settings_section = u'remotes'
self.router = HttpRouter()
def start_tcp(self):
def start_server(self):
"""
Start the http server, use the port in the settings default to 4316.
Listen out for slide and song changes so they can be broadcast to
clients. Listen out for socket connections.
Start the http server based on configuration.
"""
log.debug(u'Start TCP server')
port = Settings().value(self.plugin.settings_section + u'/port')
address = Settings().value(self.plugin.settings_section + u'/ip address')
self.server = QtNetwork.QTcpServer()
self.server.listen(QtNetwork.QHostAddress(address), port)
self.server.newConnection.connect(self.new_connection)
log.debug(u'TCP listening on port %d' % port)
log.debug(u'Start CherryPy server')
# Define to security levels and inject the router code
self.root = self.Public()
self.root.files = self.Files()
self.root.stage = self.Stage()
self.root.router = self.router
self.root.files.router = self.router
self.root.stage.router = self.router
cherrypy.tree.mount(self.root, '/', config=self.define_config())
# Turn off the flood of access messages cause by poll
cherrypy.log.access_log.propagate = False
cherrypy.engine.start()
def new_connection(self):
def define_config(self):
"""
A new http connection has been made. Create a client object to handle
communication.
Define the configuration of the server.
"""
log.debug(u'new http connection')
socket = self.server.nextPendingConnection()
if socket:
self.connections.append(HttpConnection(self, socket))
if Settings().value(self.settings_section + u'/https enabled'):
port = Settings().value(self.settings_section + u'/https port')
address = Settings().value(self.settings_section + u'/ip address')
local_data = AppLocation.get_directory(AppLocation.DataDir)
cherrypy.config.update({u'server.socket_host': str(address),
u'server.socket_port': port,
u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'),
u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')})
else:
port = Settings().value(self.settings_section + u'/port')
address = Settings().value(self.settings_section + u'/ip address')
cherrypy.config.update({u'server.socket_host': str(address)})
cherrypy.config.update({u'server.socket_port': port})
cherrypy.config.update({u'environment': u'embedded'})
cherrypy.config.update({u'engine.autoreload_on': False})
directory_config = {u'/': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'),
u'tools.basic_auth.realm': u'OpenLP Remote Login',
u'tools.basic_auth.users': fetch_password,
u'tools.basic_auth.encrypt': make_sha_hash},
u'/files': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False},
u'/stage': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False}}
return directory_config
def close_connection(self, connection):
class Public(object):
"""
The connection has been closed. Clean up
Main access class with may have security enabled on it.
"""
log.debug(u'close http connection')
if connection in self.connections:
self.connections.remove(connection)
@cherrypy.expose
def default(self, *args, **kwargs):
self.router.request_data = None
if isinstance(kwargs, dict):
self.router.request_data = kwargs.get(u'data', None)
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Files(object):
"""
Provides access to files and has no security available. These are read only accesses
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Stage(object):
"""
Stageview is read only so security is not relevant and would reduce it's usability
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
def close(self):
"""
Close down the http server.
"""
log.debug(u'close http server')
self.server.close()
cherrypy.engine.exit()
class HttpConnection(object):
class HttpRouter(object):
"""
A single connection, this handles communication between the server
and the client.
This code is called by the HttpServer upon a request and it processes it based on the routing table.
"""
def __init__(self, parent, socket):
def __init__(self):
"""
Initialise the http connection. Listen out for socket signals.
Initialise the router
"""
log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress())
self.socket = socket
self.parent = parent
self.routes = [
(u'^/$', self.serve_file),
(u'^/(stage)$', self.serve_file),
(r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll),
(r'^/stage/api/poll$', self.poll),
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service),
(r'^/stage/api/service/(.*)$', self.service),
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
(r'^/api/alert$', self.alert),
(r'^/api/plugin/(search)$', self.pluginInfo),
(r'^/api/plugin/(search)$', self.plugin_info),
(r'^/api/(.*)/search$', self.search),
(r'^/api/(.*)/live$', self.go_live),
(r'^/api/(.*)/add$', self.add_to_service)
]
self.socket.readyRead.connect(self.ready_read)
self.socket.disconnected.connect(self.disconnected)
self.translate()
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
def process_http_request(self, url_path, *args):
"""
Common function to process HTTP requests
``url_path``
The requested URL.
``*args``
Any passed data.
"""
response = None
for route, func in self.routes:
match = re.match(route, url_path)
if match:
log.debug('Route "%s" matched "%s"', route, url_path)
args = []
for param in match.groups():
args.append(param)
response = func(*args)
break
if response:
return response
else:
return self._http_not_found()
def _get_service_items(self):
"""
Read the service item in use and return the data as a json object
"""
service_items = []
if self.live_controller.service_item:
current_unique_identifier = self.live_controller.service_item.unique_identifier
@ -281,40 +357,6 @@ class HttpConnection(object):
'slides': translate('RemotePlugin.Mobile', 'Slides')
}
def ready_read(self):
"""
Data has been sent from the client. Respond to it
"""
log.debug(u'ready to read socket')
if self.socket.canReadLine():
data = str(self.socket.readLine())
try:
log.debug(u'received: ' + data)
except UnicodeDecodeError:
# Malicious request containing non-ASCII characters.
self.close()
return
words = data.split(' ')
response = None
if words[0] == u'GET':
url = urlparse.urlparse(words[1])
self.url_params = urlparse.parse_qs(url.query)
# Loop through the routes we set up earlier and execute them
for route, func in self.routes:
match = re.match(route, url.path)
if match:
log.debug('Route "%s" matched "%s"', route, url.path)
args = []
for param in match.groups():
args.append(param)
response = func(*args)
break
if response:
self.send_response(response)
else:
self.send_response(HttpResponse(code='404 Not Found'))
self.close()
def serve_file(self, filename=None):
"""
Send a file to the socket. For now, just a subset of file types
@ -329,9 +371,9 @@ class HttpConnection(object):
filename = u'index.html'
elif filename == u'stage':
filename = u'stage.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
if not path.startswith(self.parent.html_dir):
return HttpResponse(code=u'404 Not Found')
path = os.path.normpath(os.path.join(self.html_dir, filename))
if not path.startswith(self.html_dir):
return self._http_not_found()
ext = os.path.splitext(filename)[1]
html = None
if ext == u'.html':
@ -360,11 +402,12 @@ class HttpConnection(object):
content = file_handle.read()
except IOError:
log.exception(u'Failed to open %s' % path)
return HttpResponse(code=u'404 Not Found')
return self._http_not_found()
finally:
if file_handle:
file_handle.close()
return HttpResponse(content, {u'Content-Type': mimetype})
cherrypy.response.headers['Content-Type'] = mimetype
return content
def poll(self):
"""
@ -379,18 +422,20 @@ class HttpConnection(object):
u'theme': self.live_controller.theme_screen.isChecked(),
u'display': self.live_controller.desktop_screen.isChecked()
}
return HttpResponse(json.dumps({u'results': result}), {u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def display(self, action):
"""
Hide or show the display screen.
This is a cross Thread call and UI is updated so Events need to be used.
``action``
This is the action, either ``hide`` or ``show``.
"""
Registry().execute(u'slidecontroller_toggle_display', action)
return HttpResponse(json.dumps({u'results': {u'success': True}}),
{u'Content-Type': u'application/json'})
self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action)
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': True}})
def alert(self):
"""
@ -399,16 +444,16 @@ class HttpConnection(object):
plugin = self.plugin_manager.get_plugin_by_name("alerts")
if plugin.status == PluginStatus.Active:
try:
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
text = json.loads(self.request_data)[u'request'][u'text']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
text = urllib.unquote(text)
Registry().execute(u'alerts_text', [text])
self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text])
success = True
else:
success = False
return HttpResponse(json.dumps({u'results': {u'success': success}}),
{u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': success}})
def controller(self, display_type, action):
"""
@ -444,44 +489,44 @@ class HttpConnection(object):
if current_item:
json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier
else:
if self.url_params and self.url_params.get(u'data'):
if self.request_data:
try:
data = json.loads(self.url_params[u'data'][0])
data = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
log.info(data)
# This slot expects an int within a list.
id = data[u'request'][u'id']
Registry().execute(event, [id])
self.live_controller.emit(QtCore.SIGNAL(event), [data])
else:
Registry().execute(event)
self.live_controller.emit(QtCore.SIGNAL(event))
json_data = {u'results': {u'success': True}}
return HttpResponse(json.dumps(json_data), {u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps(json_data)
def service(self, action):
"""
Handles requests for service items
Handles requests for service items in the service manager
``action``
The action to perform.
"""
event = u'servicemanager_%s' % action
if action == u'list':
return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}),
{u'Content-Type': u'application/json'})
else:
event += u'_item'
if self.url_params and self.url_params.get(u'data'):
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': self._get_service_items()}})
event += u'_item'
if self.request_data:
try:
data = json.loads(self.url_params[u'data'][0])
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
Registry().execute(event, data[u'request'][u'id'])
data = json.loads(self.request_data)[u'request'][u'id']
except KeyError:
return self._http_bad_request()
self.service_manager.emit(QtCore.SIGNAL(event), data)
else:
Registry().execute(event)
return HttpResponse(json.dumps({u'results': {u'success': True}}), {u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'success': True}})
def pluginInfo(self, action):
def plugin_info(self, action):
"""
Return plugin related information, based on the action.
@ -493,8 +538,9 @@ class HttpConnection(object):
searches = []
for plugin in self.plugin_manager.plugins:
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])])
return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'})
searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])])
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': searches}})
def search(self, plugin_name):
"""
@ -504,69 +550,63 @@ class HttpConnection(object):
The plugin name to search in.
"""
try:
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
text = json.loads(self.request_data)[u'request'][u'text']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
text = urllib.unquote(text)
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
results = plugin.media_item.search(text, False)
else:
results = []
return HttpResponse(json.dumps({u'results': {u'items': results}}), {u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': results}})
def go_live(self, plugin_name):
"""
Go live on an item of type ``plugin``.
"""
try:
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id']
id = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
plugin.media_item.go_live(id, remote=True)
return HttpResponse(code=u'200 OK')
plugin.media_item.emit(QtCore.SIGNAL(u'%s_go_live' % plugin_name), [id, True])
return self._http_success()
def add_to_service(self, plugin_name):
"""
Add item of type ``plugin_name`` to the end of the service.
"""
try:
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id']
id = json.loads(self.request_data)[u'request'][u'id']
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
item_id = plugin.media_item.createItemFromId(id)
plugin.media_item.add_to_service(item_id, remote=True)
return HttpResponse(code=u'200 OK')
item_id = plugin.media_item.create_item_from_id(id)
plugin.media_item.emit(QtCore.SIGNAL(u'%s_add_to_service' % plugin_name), [item_id, True])
self._http_success()
def send_response(self, response):
http = u'HTTP/1.1 %s\r\n' % response.code
for header, value in response.headers.iteritems():
http += '%s: %s\r\n' % (header, value)
http += '\r\n'
self.socket.write(http)
self.socket.write(response.content)
def _http_success(self):
"""
Set the HTTP success return code.
"""
cherrypy.response.status = 200
def disconnected(self):
def _http_bad_request(self):
"""
The client has disconnected. Tidy up
Set the HTTP bad response return code.
"""
log.debug(u'socket disconnected')
self.close()
cherrypy.response.status = 400
def close(self):
def _http_not_found(self):
"""
The server has closed the connection. Tidy up
Set the HTTP not found return code.
"""
if not self.socket:
return
log.debug(u'close socket')
self.socket.close()
self.socket = None
self.parent.close_connection(self)
cherrypy.response.status = 404
cherrypy.response.body = ["<html><body>Sorry, an error occurred </body></html>"]
def _get_service_manager(self):
"""
@ -597,3 +637,13 @@ class HttpConnection(object):
return self._plugin_manager
plugin_manager = property(_get_plugin_manager)
def _get_alerts_manager(self):
"""
Adds the alerts manager to the class dynamically
"""
if not hasattr(self, u'_alerts_manager'):
self._alerts_manager = Registry().get(u'alerts_manager')
return self._alerts_manager
alerts_manager = property(_get_alerts_manager)

View File

@ -27,9 +27,12 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import os.path
from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.lib import Registry, Settings, SettingsTab, translate
from openlp.core.lib import Settings, SettingsTab, translate
from openlp.core.utils import AppLocation
ZERO_URL = u'0.0.0.0'
@ -53,32 +56,84 @@ class RemoteTab(SettingsTab):
self.address_label.setObjectName(u'address_label')
self.address_edit = QtGui.QLineEdit(self.server_settings_group_box)
self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(
u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self))
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
self))
self.address_edit.setObjectName(u'address_edit')
self.server_settings_layout.addRow(self.address_label, self.address_edit)
self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box')
self.server_settings_layout.addRow(self.twelve_hour_check_box)
self.port_label = QtGui.QLabel(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.setObjectName(u'http_settings_group_box')
self.http_setting_layout = QtGui.QFormLayout(self.http_settings_group_box)
self.http_setting_layout.setObjectName(u'http_setting_layout')
self.port_label = QtGui.QLabel(self.http_settings_group_box)
self.port_label.setObjectName(u'port_label')
self.port_spin_box = QtGui.QSpinBox(self.server_settings_group_box)
self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box)
self.port_spin_box.setMaximum(32767)
self.port_spin_box.setObjectName(u'port_spin_box')
self.server_settings_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtGui.QLabel(self.server_settings_group_box)
self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
self.remote_url_label = QtGui.QLabel(self.http_settings_group_box)
self.remote_url_label.setObjectName(u'remote_url_label')
self.remote_url = QtGui.QLabel(self.server_settings_group_box)
self.remote_url = QtGui.QLabel(self.http_settings_group_box)
self.remote_url.setObjectName(u'remote_url')
self.remote_url.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.remote_url_label, self.remote_url)
self.stage_url_label = QtGui.QLabel(self.server_settings_group_box)
self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
self.stage_url_label = QtGui.QLabel(self.http_settings_group_box)
self.stage_url_label.setObjectName(u'stage_url_label')
self.stage_url = QtGui.QLabel(self.server_settings_group_box)
self.stage_url = QtGui.QLabel(self.http_settings_group_box)
self.stage_url.setObjectName(u'stage_url')
self.stage_url.setOpenExternalLinks(True)
self.server_settings_layout.addRow(self.stage_url_label, self.stage_url)
self.left_layout.addWidget(self.server_settings_group_box)
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
self.left_layout.addWidget(self.http_settings_group_box)
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
self.https_settings_group_box.setCheckable(True)
self.https_settings_group_box.setChecked(False)
self.https_settings_group_box.setObjectName(u'https_settings_group_box')
self.https_settings_layout = QtGui.QFormLayout(self.https_settings_group_box)
self.https_settings_layout.setObjectName(u'https_settings_layout')
self.https_error_label = QtGui.QLabel(self.https_settings_group_box)
self.https_error_label.setVisible(False)
self.https_error_label.setWordWrap(True)
self.https_error_label.setObjectName(u'https_error_label')
self.https_settings_layout.addRow(self.https_error_label)
self.https_port_label = QtGui.QLabel(self.https_settings_group_box)
self.https_port_label.setObjectName(u'https_port_label')
self.https_port_spin_box = QtGui.QSpinBox(self.https_settings_group_box)
self.https_port_spin_box.setMaximum(32767)
self.https_port_spin_box.setObjectName(u'https_port_spin_box')
self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box)
self.remote_https_url = QtGui.QLabel(self.https_settings_group_box)
self.remote_https_url.setObjectName(u'remote_http_url')
self.remote_https_url.setOpenExternalLinks(True)
self.remote_https_url_label = QtGui.QLabel(self.https_settings_group_box)
self.remote_https_url_label.setObjectName(u'remote_http_url_label')
self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url)
self.stage_https_url_label = QtGui.QLabel(self.http_settings_group_box)
self.stage_https_url_label.setObjectName(u'stage_https_url_label')
self.stage_https_url = QtGui.QLabel(self.https_settings_group_box)
self.stage_https_url.setObjectName(u'stage_https_url')
self.stage_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
self.left_layout.addWidget(self.https_settings_group_box)
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
self.user_login_group_box.setCheckable(True)
self.user_login_group_box.setChecked(False)
self.user_login_group_box.setObjectName(u'user_login_group_box')
self.user_login_layout = QtGui.QFormLayout(self.user_login_group_box)
self.user_login_layout.setObjectName(u'user_login_layout')
self.user_id_label = QtGui.QLabel(self.user_login_group_box)
self.user_id_label.setObjectName(u'user_id_label')
self.user_id = QtGui.QLineEdit(self.user_login_group_box)
self.user_id.setObjectName(u'user_id')
self.user_login_layout.addRow(self.user_id_label, self.user_id)
self.password_label = QtGui.QLabel(self.user_login_group_box)
self.password_label.setObjectName(u'password_label')
self.password = QtGui.QLineEdit(self.user_login_group_box)
self.password.setObjectName(u'password')
self.user_login_layout.addRow(self.password_label, self.password)
self.left_layout.addWidget(self.user_login_group_box)
self.android_app_group_box = QtGui.QGroupBox(self.right_column)
self.android_app_group_box.setObjectName(u'android_app_group_box')
self.right_layout.addWidget(self.android_app_group_box)
@ -96,9 +151,11 @@ class RemoteTab(SettingsTab):
self.qr_layout.addWidget(self.qr_description_label)
self.left_layout.addStretch()
self.right_layout.addStretch()
self.twelve_hour_check_box.stateChanged.connect(self.onTwelveHourCheckBoxChanged)
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
self.address_edit.textChanged.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_settings_group_box.clicked.connect(self.https_changed)
def retranslateUi(self):
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
@ -112,8 +169,21 @@ class RemoteTab(SettingsTab):
'Scan the QR code or click <a href="https://play.google.com/store/'
'apps/details?id=org.openlp.android">download</a> to install the '
'Android app from Google Play.'))
self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
self.https_error_label.setText(translate('RemotePlugin.RemoteTab',
'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate '
'is found. Please see the manual for more information.'))
self.https_port_label.setText(self.port_label.text())
self.remote_https_url_label.setText(self.remote_url_label.text())
self.stage_https_url_label.setText(self.stage_url_label.text())
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
def set_urls(self):
"""
Update the display based on the data input on the screen
"""
ip_address = u'localhost'
if self.address_edit.text() == ZERO_URL:
interfaces = QtNetwork.QNetworkInterface.allInterfaces()
@ -129,31 +199,73 @@ class RemoteTab(SettingsTab):
break
else:
ip_address = self.address_edit.text()
url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (url, url))
url += u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (url, url))
http_url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
http_url += u'stage'
https_url += u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
def load(self):
"""
Load the configuration and update the server configuration if necessary
"""
self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port'))
self.address_edit.setText(Settings().value(self.settings_section + u'/ip address'))
self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour')
self.twelve_hour_check_box.setChecked(self.twelve_hour)
local_data = AppLocation.get_directory(AppLocation.DataDir)
if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \
not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')):
self.https_settings_group_box.setChecked(False)
self.https_settings_group_box.setEnabled(False)
self.https_error_label.setVisible(True)
else:
self.https_settings_group_box.setChecked(Settings().value(self.settings_section + u'/https enabled'))
self.https_settings_group_box.setEnabled(True)
self.https_error_label.setVisible(False)
self.user_login_group_box.setChecked(Settings().value(self.settings_section + u'/authentication enabled'))
self.user_id.setText(Settings().value(self.settings_section + u'/user id'))
self.password.setText(Settings().value(self.settings_section + u'/password'))
self.set_urls()
self.https_changed()
def save(self):
changed = False
"""
Save the configuration and update the server configuration if necessary
"""
if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value():
changed = True
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \
Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + u'/https enabled') != \
self.https_settings_group_box.isChecked() or \
Settings().value(self.settings_section + u'/authentication enabled') != \
self.user_login_group_box.isChecked():
self.settings_form.register_post_process(u'remotes_config_updated')
Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value())
Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked())
Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text())
Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour)
if changed:
Registry().execute(u'remotes_config_updated')
Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + u'/user id', self.user_id.text())
Settings().setValue(self.settings_section + u'/password', self.password.text())
def onTwelveHourCheckBoxChanged(self, check_state):
def on_twelve_hour_check_box_changed(self, check_state):
"""
Toggle the 12 hour check box.
"""
self.twelve_hour = False
# we have a set value convert to True/False
if check_state == QtCore.Qt.Checked:
self.twelve_hour = True
def https_changed(self):
"""
Invert the HTTP group box based on Https group settings
"""
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())

View File

@ -29,6 +29,8 @@
import logging
from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, HttpServer
@ -37,6 +39,11 @@ log = logging.getLogger(__name__)
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
@ -60,7 +67,8 @@ class RemotesPlugin(Plugin):
"""
log.debug(u'initialise')
Plugin.initialise(self)
self.server = HttpServer(self)
self.server = HttpServer()
self.server.start_server()
def finalise(self):
"""
@ -70,6 +78,7 @@ class RemotesPlugin(Plugin):
Plugin.finalise(self)
if self.server:
self.server.close()
self.server = None
def about(self):
"""
@ -99,5 +108,6 @@ class RemotesPlugin(Plugin):
"""
Called when Config is changed to restart the server on new address or port
"""
self.finalise()
self.initialise()
log.debug(u'remote config changed')
self.main_window.information_message(translate('RemotePlugin', 'Configuration Change'),
translate('RemotePlugin', 'OpenLP will need to be restarted for the Remote changes to become active.'))

View File

@ -320,7 +320,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
for plugin in self.plugin_manager.plugins:
if plugin.name == u'media' and plugin.status == PluginStatus.Active:
self.from_media_button.setVisible(True)
self.media_form.populateFiles(plugin.media_item.getList(MediaType.Audio))
self.media_form.populateFiles(plugin.media_item.get_list(MediaType.Audio))
break
def new_song(self):

View File

@ -37,7 +37,6 @@ from PyQt4 import QtCore, QtGui
from openlp.core.lib import Registry, UiStrings, create_separated_list, build_icon, translate
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
from openlp.plugins.songs.lib import natcmp
from openlp.plugins.songs.lib.db import Song
from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport
@ -222,7 +221,7 @@ class SongExportForm(OpenLPWizard):
# Load the list of songs.
self.application.set_busy_cursor()
songs = self.plugin.manager.get_all_objects(Song)
songs.sort(cmp=natcmp, key=lambda song: song.sort_key)
songs.sort(key=lambda song: song.sort_key)
for song in songs:
# No need to export temporary songs.
if song.temporary:

View File

@ -34,7 +34,7 @@ import re
from PyQt4 import QtGui
from openlp.core.lib import translate
from openlp.core.utils import CONTROL_CHARS, locale_direct_compare
from openlp.core.utils import CONTROL_CHARS
from db import Author
from ui import SongStrings
@ -168,6 +168,7 @@ class VerseType(object):
translate('SongsPlugin.VerseType', 'Intro'),
translate('SongsPlugin.VerseType', 'Ending'),
translate('SongsPlugin.VerseType', 'Other')]
translated_tags = [name[0].lower() for name in translated_names]
@staticmethod
@ -592,37 +593,3 @@ def strip_rtf(text, default_encoding=None):
text = u''.join(out)
return text, default_encoding
def natcmp(a, b):
"""
Natural string comparison which mimics the behaviour of Python's internal cmp function.
"""
if len(a) <= len(b):
for i, key in enumerate(a):
if isinstance(key, int) and isinstance(b[i], int):
result = cmp(key, b[i])
elif isinstance(key, int) and not isinstance(b[i], int):
result = locale_direct_compare(str(key), b[i])
elif not isinstance(key, int) and isinstance(b[i], int):
result = locale_direct_compare(key, str(b[i]))
else:
result = locale_direct_compare(key, b[i])
if result != 0:
return result
if len(a) == len(b):
return 0
else:
return -1
else:
for i, key in enumerate(b):
if isinstance(a[i], int) and isinstance(key, int):
result = cmp(a[i], key)
elif isinstance(a[i], int) and not isinstance(key, int):
result = locale_direct_compare(str(a[i]), key)
elif not isinstance(a[i], int) and isinstance(key, int):
result = locale_direct_compare(a[i], str(key))
else:
result = locale_direct_compare(a[i], key)
if result != 0:
return result
return 1

View File

@ -38,6 +38,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.sql.expression import func
from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key
class Author(BaseModel):
@ -69,36 +70,15 @@ class Song(BaseModel):
def __init__(self):
self.sort_key = ()
def _try_int(self, s):
"""
Convert to integer if possible.
"""
try:
return int(s)
except:
return s.lower()
def _natsort_key(self, s):
"""
Used internally to get a tuple by which s is sorted.
"""
return map(self._try_int, re.findall(r'(\d+|\D+)', s))
# This decorator tells sqlalchemy to call this method everytime
# any data on this object is updated.
@reconstructor
def init_on_load(self):
"""
Precompute a tuple to be used for sorting.
Precompute a natural sorting, locale aware sorting key.
Song sorting is performance sensitive operation.
To get maximum speed lets precompute the string
used for comparison.
To get maximum speed lets precompute the sorting key.
"""
# Avoid the overhead of converting string to lowercase and to QString
# with every call to sort().
self.sort_key = self._natsort_key(self.title)
self.sort_key = get_natural_key(self.title)
class Topic(BaseModel):

View File

@ -43,7 +43,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, natcmp
from openlp.plugins.songs.lib import VerseType, clean_string
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@ -225,7 +225,7 @@ class SongMediaItem(MediaManagerItem):
log.debug(u'display results Song')
self.save_auto_select_id()
self.list_view.clear()
searchresults.sort(cmp=natcmp, key=lambda song: song.sort_key)
searchresults.sort(key=lambda song: song.sort_key)
for song in searchresults:
# Do not display temporary songs
if song.temporary:
@ -467,9 +467,9 @@ class SongMediaItem(MediaManagerItem):
service_item.raw_footer.append(song.title)
service_item.raw_footer.append(create_separated_list(author_list))
service_item.raw_footer.append(song.copyright)
if Settings().value(u'general/ccli number'):
if Settings().value(u'core/ccli number'):
service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
Settings().value(u'general/ccli number'))
Settings().value(u'core/ccli number'))
service_item.audit = [
song.title, author_list, song.copyright, unicode(song.ccli_number)
]

View File

@ -260,7 +260,10 @@ class SongImport(QtCore.QObject):
elif int(verse_def[1:]) > self.verseCounts[verse_def[0]]:
self.verseCounts[verse_def[0]] = int(verse_def[1:])
self.verses.append([verse_def, verse_text.rstrip(), lang])
self.verseOrderListGenerated.append(verse_def)
# A verse_def refers to all verses with that name, adding it once adds every instance, so do not add if already
# used.
if verse_def not in self.verseOrderListGenerated:
self.verseOrderListGenerated.append(verse_def)
def repeatVerse(self):
"""

View File

@ -32,6 +32,7 @@ SongShow Plus songs into the OpenLP database.
"""
import os
import logging
import re
import struct
from openlp.core.ui.wizard import WizardStrings
@ -44,43 +45,36 @@ COPYRIGHT = 3
CCLI_NO = 5
VERSE = 12
CHORUS = 20
BRIDGE = 24
TOPIC = 29
COMMENTS = 30
VERSE_ORDER = 31
SONG_BOOK = 35
SONG_NUMBER = 36
CUSTOM_VERSE = 37
BRIDGE = 24
log = logging.getLogger(__name__)
class SongShowPlusImport(SongImport):
"""
The :class:`SongShowPlusImport` class provides the ability to import song
files from SongShow Plus.
The :class:`SongShowPlusImport` class provides the ability to import song files from SongShow Plus.
**SongShow Plus Song File Format:**
The SongShow Plus song file format is as follows:
* Each piece of data in the song file has some information that precedes
it.
* Each piece of data in the song file has some information that precedes it.
* The general format of this data is as follows:
4 Bytes, forming a 32 bit number, a key if you will, this describes what
the data is (see blockKey below)
4 Bytes, forming a 32 bit number, which is the number of bytes until the
next block starts
4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
1 Byte, which tells how many bytes follows
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string
is less than 255
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
The next bytes are the actual data.
The next block of data follows on.
This description does differ for verses. Which includes extra bytes
stating the verse type or number. In some cases a "custom" verse is used,
in that case, this block will in include 2 strings, with the associated
string length descriptors. The first string is the name of the verse, the
second is the verse content.
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some cases
a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string length
descriptors. The first string is the name of the verse, the second is the verse content.
The file is ended with four null bytes.
@ -88,8 +82,9 @@ class SongShowPlusImport(SongImport):
* .sbsong
"""
otherList = {}
otherCount = 0
other_count = 0
other_list = {}
def __init__(self, manager, **kwargs):
"""
@ -107,9 +102,9 @@ class SongShowPlusImport(SongImport):
for file in self.import_source:
if self.stop_import_flag:
return
self.sspVerseOrderList = []
other_count = 0
other_list = {}
self.ssp_verse_order_list = []
self.other_count = 0
self.other_list = {}
file_name = os.path.split(file)[1]
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % file_name, 0)
song_data = open(file, 'rb')
@ -162,34 +157,37 @@ class SongShowPlusImport(SongImport):
elif block_key == COMMENTS:
self.comments = unicode(data, u'cp1252')
elif block_key == VERSE_ORDER:
verse_tag = self.toOpenLPVerseTag(data, True)
verse_tag = self.to_openlp_verse_tag(data, True)
if verse_tag:
if not isinstance(verse_tag, unicode):
verse_tag = unicode(verse_tag, u'cp1252')
self.sspVerseOrderList.append(verse_tag)
self.ssp_verse_order_list.append(verse_tag)
elif block_key == SONG_BOOK:
self.songBookName = unicode(data, u'cp1252')
elif block_key == SONG_NUMBER:
self.songNumber = ord(data)
elif block_key == CUSTOM_VERSE:
verse_tag = self.toOpenLPVerseTag(verse_name)
verse_tag = self.to_openlp_verse_tag(verse_name)
self.addVerse(unicode(data, u'cp1252'), verse_tag)
else:
log.debug("Unrecognised blockKey: %s, data: %s" % (block_key, data))
song_data.seek(next_block_starts)
self.verseOrderList = self.sspVerseOrderList
self.verseOrderList = self.ssp_verse_order_list
song_data.close()
if not self.finish():
self.logError(file)
def toOpenLPVerseTag(self, verse_name, ignore_unique=False):
if verse_name.find(" ") != -1:
verse_parts = verse_name.split(" ")
verse_type = verse_parts[0]
verse_number = verse_parts[1]
def to_openlp_verse_tag(self, verse_name, ignore_unique=False):
# Have we got any digits? If so, verse number is everything from the digits to the end (OpenLP does not have
# concept of part verses, so just ignore any non integers on the end (including floats))
match = re.match(r'(\D*)(\d+)', verse_name)
if match:
verse_type = match.group(1).strip()
verse_number = match.group(2)
else:
# otherwise we assume number 1 and take the whole prefix as the verse tag
verse_type = verse_name
verse_number = "1"
verse_number = u'1'
verse_type = verse_type.lower()
if verse_type == "verse":
verse_tag = VerseType.tags[VerseType.Verse]
@ -200,11 +198,11 @@ class SongShowPlusImport(SongImport):
elif verse_type == "pre-chorus":
verse_tag = VerseType.tags[VerseType.PreChorus]
else:
if verse_name not in self.otherList:
if verse_name not in self.other_list:
if ignore_unique:
return None
self.otherCount += 1
self.otherList[verse_name] = str(self.otherCount)
self.other_count += 1
self.other_list[verse_name] = str(self.other_count)
verse_tag = VerseType.tags[VerseType.Other]
verse_number = self.otherList[verse_name]
verse_number = self.other_list[verse_name]
return verse_tag + verse_number

View File

@ -27,7 +27,6 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`songusage` module contains the Song Usage plugin. The Song Usage
plugin provides auditing capabilities for reporting the songs you are using to
copyright license organisations.
The :mod:`songusage` module contains the Song Usage plugin. The Song Usage plugin provides auditing capabilities for
reporting the songs you are using to copyright license organisations.
"""

View File

@ -55,7 +55,8 @@ class Ui_SongUsageDeleteDialog(object):
self.retranslateUi(song_usage_delete_dialog)
def retranslateUi(self, song_usage_delete_dialog):
song_usage_delete_dialog.setWindowTitle(translate('SongUsagePlugin.SongUsageDeleteForm', 'Delete Song Usage Data'))
song_usage_delete_dialog.setWindowTitle(
translate('SongUsagePlugin.SongUsageDeleteForm', 'Delete Song Usage Data'))
self.delete_label.setText(
translate('SongUsagePlugin.SongUsageDeleteForm', 'Select the date up to which the song usage data '
'should be deleted. All data recorded before this date will be permanently deleted.'))

View File

@ -81,7 +81,8 @@ class Ui_SongUsageDetailDialog(object):
self.save_file_push_button.clicked.connect(song_usage_detail_dialog.define_output_location)
def retranslateUi(self, song_usage_detail_dialog):
song_usage_detail_dialog.setWindowTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Song Usage Extraction'))
song_usage_detail_dialog.setWindowTitle(
translate('SongUsagePlugin.SongUsageDetailForm', 'Song Usage Extraction'))
self.date_range_group_box.setTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Select Date Range'))
self.to_label.setText(translate('SongUsagePlugin.SongUsageDetailForm', 'to'))
self.file_group_box.setTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Location'))

View File

@ -36,12 +36,14 @@ from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db
class SongUsageItem(BaseModel):
"""
SongUsageItem model
"""
pass
def init_schema(url):
"""
Setup the songusage database connection and initialise the database schema

View File

@ -48,11 +48,11 @@ if QtCore.QDate().currentDate().month() < 9:
__default_settings__ = {
u'songusage/db type': u'sqlite',
u'songusage/active': False,
u'songusage/to date': QtCore.QDate(YEAR, 8, 31),
u'songusage/from date': QtCore.QDate(YEAR - 1, 9, 1),
u'songusage/last directory export': u''
u'songusage/db type': u'sqlite',
u'songusage/active': False,
u'songusage/to date': QtCore.QDate(YEAR, 8, 31),
u'songusage/from date': QtCore.QDate(YEAR - 1, 9, 1),
u'songusage/last directory export': u''
}
@ -76,12 +76,10 @@ class SongUsagePlugin(Plugin):
def add_tools_menu_item(self, tools_menu):
"""
Give the SongUsage plugin the opportunity to add items to the
**Tools** menu.
Give the SongUsage plugin the opportunity to add items to the **Tools** menu.
``tools_menu``
The actual **Tools** menu item, so that your actions can
use it as their parent.
The actual **Tools** menu item, so that your actions can use it as their parent.
"""
log.info(u'add tools menu')
self.toolsMenu = tools_menu
@ -218,8 +216,8 @@ class SongUsagePlugin(Plugin):
self.song_usage_detail_form.exec_()
def about(self):
about_text = translate('SongUsagePlugin', '<strong>SongUsage Plugin'
'</strong><br />This plugin tracks the usage of songs in services.')
about_text = translate('SongUsagePlugin',
'<strong>SongUsage Plugin</strong><br />This plugin tracks the usage of songs in services.')
return about_text
def set_plugin_text_strings(self):

View File

@ -81,8 +81,10 @@ MODULES = [
'enchant',
'bs4',
'mako',
'cherrypy',
'migrate',
'uno',
'icu',
]

View File

@ -15,7 +15,7 @@ class TestLib(TestCase):
"""
Test the str_to_bool function with boolean input
"""
#GIVEN: A boolean value set to true
# GIVEN: A boolean value set to true
true_boolean = True
# WHEN: We "convert" it to a bool
@ -25,7 +25,7 @@ class TestLib(TestCase):
assert isinstance(true_result, bool), u'The result should be a boolean'
assert true_result is True, u'The result should be True'
#GIVEN: A boolean value set to false
# GIVEN: A boolean value set to false
false_boolean = False
# WHEN: We "convert" it to a bool

View File

@ -74,7 +74,7 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should have been called
# THEN: The hook_settings_tabs() method should have been called
assert mocked_plugin.create_media_manager_item.call_count == 0, \
u'The create_media_manager_item() method should not have been called.'
@ -94,8 +94,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same
assert mocked_plugin.create_settings_Tab.call_count == 0, \
# THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same
assert mocked_plugin.create_settings_tab.call_count == 0, \
u'The create_media_manager_item() method should not have been called.'
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@ -117,7 +117,7 @@ class TestPluginManager(TestCase):
plugin_manager.hook_settings_tabs()
# THEN: The create_media_manager_item() method should have been called with the mocked settings form
assert mocked_plugin.create_settings_Tab.call_count == 1, \
assert mocked_plugin.create_settings_tab.call_count == 1, \
u'The create_media_manager_item() method should have been called once.'
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@ -135,8 +135,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_settings_tabs()
plugin_manager.hook_settings_tabs()
# THEN: The create_settings_Tab() method should have been called
mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form)
# THEN: The create_settings_tab() method should have been called
mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form)
def hook_import_menu_with_disabled_plugin_test(self):
"""

View File

@ -276,5 +276,7 @@ class TestServiceItem(TestCase):
first_line = items[0]
except IOError:
first_line = u''
finally:
open_file.close()
return first_line

View File

@ -11,7 +11,9 @@ from PyQt4 import QtGui
class TestSettings(TestCase):
"""
Test the functions in the Settings module
"""
def setUp(self):
"""
Create the UI
@ -35,16 +37,16 @@ class TestSettings(TestCase):
# GIVEN: A new Settings setup
# WHEN reading a setting for the first time
default_value = Settings().value(u'general/has run wizard')
default_value = Settings().value(u'core/has run wizard')
# THEN the default value is returned
assert default_value is False, u'The default value should be False'
# WHEN a new value is saved into config
Settings().setValue(u'general/has run wizard', True)
Settings().setValue(u'core/has run wizard', True)
# THEN the new value is returned when re-read
assert Settings().value(u'general/has run wizard') is True, u'The saved value should have been returned'
assert Settings().value(u'core/has run wizard') is True, u'The saved value should have been returned'
def settings_override_test(self):
"""

View File

@ -6,6 +6,7 @@ from unittest import TestCase
from openlp.core.lib import UiStrings
class TestUiStrings(TestCase):
def check_same_instance_test(self):

View File

@ -30,8 +30,10 @@ class TestAppLocation(TestCase):
mocked_get_directory.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = u'test/dir'
# WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_data_path()
# THEN: check that all the correct methods were called, and the result is correct
mocked_settings.contains.assert_called_with(u'advanced/data path')
mocked_get_directory.assert_called_with(AppLocation.DataDir)
@ -49,8 +51,10 @@ class TestAppLocation(TestCase):
mocked_settings.contains.return_value = True
mocked_settings.value.return_value.toString.return_value = u'custom/dir'
mocked_os.path.normpath.return_value = u'custom/dir'
# WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_data_path()
# THEN: the mocked Settings methods were called and the value returned was our set up value
mocked_settings.contains.assert_called_with(u'advanced/data path')
mocked_settings.value.assert_called_with(u'advanced/data path')
@ -100,8 +104,10 @@ class TestAppLocation(TestCase):
# GIVEN: A mocked out AppLocation.get_data_path()
mocked_get_data_path.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True
# WHEN: we call AppLocation.get_data_path()
data_path = AppLocation.get_section_data_path(u'section')
# THEN: check that all the correct methods were called, and the result is correct
mocked_check_directory_exists.assert_called_with(u'test/dir/section')
assert data_path == u'test/dir/section', u'Result should be "test/dir/section"'
@ -112,8 +118,10 @@ class TestAppLocation(TestCase):
"""
with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path:
mocked_get_frozen_path.return_value = u'app/dir'
# WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.AppDir)
# THEN:
assert directory == u'app/dir', u'Directory should be "app/dir"'
@ -130,8 +138,10 @@ class TestAppLocation(TestCase):
mocked_get_frozen_path.return_value = u'plugins/dir'
mocked_sys.frozen = 1
mocked_sys.argv = ['openlp']
# WHEN: We call AppLocation.get_directory
directory = AppLocation.get_directory(AppLocation.PluginsDir)
# THEN:
assert directory == u'plugins/dir', u'Directory should be "plugins/dir"'

View File

@ -5,7 +5,9 @@ from unittest import TestCase
from mock import patch
from openlp.core.utils import get_filesystem_encoding, _get_frozen_path
from openlp.core.utils import clean_filename, get_filesystem_encoding, _get_frozen_path, get_locale_key, \
get_natural_key, split_filename
class TestUtils(TestCase):
"""
@ -56,3 +58,76 @@ class TestUtils(TestCase):
# THEN: The frozen parameter is returned
assert _get_frozen_path(u'frozen', u'not frozen') == u'frozen', u'Should return "frozen"'
def split_filename_with_file_path_test(self):
"""
Test the split_filename() function with a path to a file
"""
# GIVEN: A path to a file.
file_path = u'/home/user/myfile.txt'
wanted_result = (u'/home/user', u'myfile.txt')
with patch(u'openlp.core.utils.os.path.isfile') as mocked_is_file:
mocked_is_file.return_value = True
# WHEN: Split the file name.
result = split_filename(file_path)
# THEN: A tuple should be returned.
assert result == wanted_result, u'A tuple with the directory and file name should have been returned.'
def split_filename_with_dir_path_test(self):
"""
Test the split_filename() function with a path to a directory
"""
# GIVEN: A path to a dir.
file_path = u'/home/user/mydir'
wanted_result = (u'/home/user/mydir', u'')
with patch(u'openlp.core.utils.os.path.isfile') as mocked_is_file:
mocked_is_file.return_value = False
# WHEN: Split the file name.
result = split_filename(file_path)
# THEN: A tuple should be returned.
assert result == wanted_result, \
u'A two-entry tuple with the directory and file name (empty) should have been returned.'
def clean_filename_test(self):
"""
Test the clean_filename() function
"""
# GIVEN: A invalid file name and the valid file name.
invalid_name = u'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py'
wanted_name = u'A_file_with_invalid_characters______________________.py'
# WHEN: Clean the name.
result = clean_filename(invalid_name)
# THEN: The file name should be cleaned.
assert result == wanted_name, u'The file name should not contain any special characters.'
def get_locale_key_test(self):
"""
Test the get_locale_key(string) function
"""
with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
# GIVEN: The language is German
# 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
mocked_get_language.return_value = u'de'
unsorted_list = [u'Auszug', u'Aushang', u'\u00C4u\u00DFerung']
# WHEN: We sort the list and use get_locale_key() to generate the sorting keys
# THEN: We get a properly sorted list
test_passes = sorted(unsorted_list, key=get_locale_key) == [u'Aushang', u'\u00C4u\u00DFerung', u'Auszug']
assert test_passes, u'Strings should be sorted properly'
def get_natural_key_test(self):
"""
Test the get_natural_key(string) function
"""
with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
# GIVEN: The language is English (a language, which sorts digits before letters)
mocked_get_language.return_value = u'en'
unsorted_list = [u'item 10a', u'item 3b', u'1st item']
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
# THEN: We get a properly sorted list
test_passes = sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a']
assert test_passes, u'Numbers should be sorted naturally'

View File

@ -35,7 +35,7 @@ class TestImageMediaItem(TestCase):
"""
# GIVEN: An empty image_list
image_list = []
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the empty list
@ -47,37 +47,37 @@ class TestImageMediaItem(TestCase):
def save_new_images_list_single_image_with_reload_test(self):
"""
Test that the save_new_images_list() calls loadFullList() when reload_list is set to True
Test that the save_new_images_list() calls load_full_list() when reload_list is set to True
"""
# GIVEN: A list with 1 image
image_list = [ u'test_image.jpg' ]
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
ImageFilenames.filename = ''
self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with reload_list=True
self.media_item.save_new_images_list(image_list, reload_list=True)
# THEN: loadFullList() should have been called
assert mocked_loadFullList.call_count == 1, u'loadFullList() should have been called'
# THEN: load_full_list() should have been called
assert mocked_loadFullList.call_count == 1, u'load_full_list() should have been called'
# CLEANUP: Remove added attribute from ImageFilenames
delattr(ImageFilenames, 'filename')
def save_new_images_list_single_image_without_reload_test(self):
"""
Test that the save_new_images_list() doesn't call loadFullList() when reload_list is set to False
Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False
"""
# GIVEN: A list with 1 image
image_list = [ u'test_image.jpg' ]
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with reload_list=False
self.media_item.save_new_images_list(image_list, reload_list=False)
# THEN: loadFullList() should not have been called
assert mocked_loadFullList.call_count == 0, u'loadFullList() should not have been called'
# THEN: load_full_list() should not have been called
assert mocked_loadFullList.call_count == 0, u'load_full_list() should not have been called'
def save_new_images_list_multiple_images_test(self):
"""
@ -85,15 +85,15 @@ class TestImageMediaItem(TestCase):
"""
# GIVEN: A list with 3 images
image_list = [ u'test_image_1.jpg', u'test_image_2.jpg', u'test_image_3.jpg' ]
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the list of 3 images
self.media_item.save_new_images_list(image_list, reload_list=False)
# THEN: loadFullList() should not have been called
# THEN: load_full_list() should not have been called
assert self.media_item.manager.save_object.call_count == 3, \
u'loadFullList() should have been called three times'
u'load_full_list() should have been called three times'
def save_new_images_list_other_objects_in_list_test(self):
"""
@ -101,12 +101,12 @@ class TestImageMediaItem(TestCase):
"""
# GIVEN: A list with images and objects
image_list = [ u'test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg' ]
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the list of images and objects
self.media_item.save_new_images_list(image_list, reload_list=False)
# THEN: loadFullList() should not have been called
# THEN: load_full_list() should not have been called
assert self.media_item.manager.save_object.call_count == 2, \
u'loadFullList() should have been called only once'
u'load_full_list() should have been called only once'

View File

@ -0,0 +1,108 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import patch
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.remotetab import RemoteTab
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
ZERO_URL = u'0.0.0.0'
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources'))
class TestRemoteTab(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.parent = QtGui.QMainWindow()
self.form = RemoteTab(self.parent, u'Remotes', None, None)
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
del self.parent
del self.form
os.unlink(self.ini_file)
def set_basic_urls_test(self):
"""
Test the set_urls function with standard defaults
"""
# GIVEN: A mocked location
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
patch(u'openlp.core.utils.applocation.os') as mocked_os:
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
mocked_settings = mocked_class.return_value
mocked_settings.contains.return_value = False
mocked_get_directory.return_value = u'test/dir'
mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = u'test/dir'
# WHEN: when the set_urls is called having reloaded the form.
self.form.load()
self.form.set_urls()
# THEN: the following screen values should be set
self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen')
self.assertEqual(self.form.https_settings_group_box.isEnabled(), False,
u'The Https box should not be enabled')
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
u'The Https checked box should note be Checked')
self.assertEqual(self.form.user_login_group_box.isChecked(), False,
u'The authentication box should not be enabled')
def set_certificate_urls_test(self):
"""
Test the set_urls function with certificate available
"""
# GIVEN: A mocked location
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
patch(u'openlp.core.utils.applocation.os') as mocked_os:
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
mocked_settings = mocked_class.return_value
mocked_settings.contains.return_value = False
mocked_get_directory.return_value = TEST_PATH
mocked_check_directory_exists.return_value = True
mocked_os.path.normpath.return_value = TEST_PATH
# WHEN: when the set_urls is called having reloaded the form.
self.form.load()
self.form.set_urls()
# THEN: the following screen values should be set
self.assertEqual(self.form.http_settings_group_box.isEnabled(), True,
u'The Http group box should be enabled')
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
u'The Https checked box should be Checked')
self.assertEqual(self.form.https_settings_group_box.isEnabled(), True,
u'The Https box should be enabled')

View File

@ -0,0 +1,99 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import MagicMock
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
class TestRouter(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.router = HttpRouter()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
os.unlink(self.ini_file)
def fetch_password_unknown_test(self):
"""
Test the fetch password code with an unknown userid
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
password = fetch_password(u'itwinkle')
# THEN: the function should return None
self.assertEqual(password, None, u'The result for fetch_password should be None')
def fetch_password_known_test(self):
"""
Test the fetch password code with the defined userid
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
password = fetch_password(u'openlp')
required_password = make_sha_hash(u'password')
# THEN: the function should return the correct password
self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password')
def sha_password_encrypter_test(self):
"""
Test hash password function
"""
# GIVEN: A default configuration
# WHEN: called with the defined userid
required_password = make_sha_hash(u'password')
test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
# THEN: the function should return the correct password
self.assertEqual(required_password, test_value,
u'The result for make_sha_hash should return the correct encrypted password')
def process_http_request_test(self):
"""
Test the router control functionality
"""
# GIVEN: A testing set of Routes
mocked_function = MagicMock()
test_route = [
(r'^/stage/api/poll$', mocked_function),
]
self.router.routes = test_route
# WHEN: called with a poll route
self.router.process_http_request(u'/stage/api/poll', None)
# THEN: the function should have been called only once
assert mocked_function.call_count == 1, \
u'The mocked function should have been matched and called once.'

View File

@ -0,0 +1,235 @@
"""
This module contains tests for the SongShow Plus song importer.
"""
import os
from unittest import TestCase
from mock import patch, MagicMock
from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib.songshowplusimport import SongShowPlusImport
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'../../../resources/songshowplussongs'))
SONG_TEST_DATA = {u'Amazing Grace.sbsong':
{u'title': u'Amazing Grace (Demonstration)',
u'authors': [u'John Newton', u'Edwin Excell', u'John P. Rees'],
u'copyright': u'Public Domain ',
u'ccli_number': 22025,
u'verses':
[(u'Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\n'
u'I once was lost, but now am found;\r\nWas blind, but now I see.', u'v1'),
(u'\'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n'
u'How precious did that grace appear,\r\nThe hour I first believed.', u'v2'),
(u'The Lord has promised good to me,\r\nHis Word my hope secures.\r\n'
u'He will my shield and portion be\r\nAs long as life endures.', u'v3'),
(u'Thro\' many dangers, toils and snares\r\nI have already come.\r\n'
u'\'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.', u'v4'),
(u'When we\'ve been there ten thousand years,\r\nBright shining as the sun,\r\n'
u'We\'ve no less days to sing God\'s praise,\r\nThan when we first begun.', u'v5')],
u'topics': [u'Assurance', u'Grace', u'Praise', u'Salvation'],
u'comments': u'\n\n\n',
u'song_book_name': u'Demonstration Songs',
u'song_number': 0,
u'verse_order_list': []},
u'Beautiful Garden Of Prayer.sbsong':
{u'title': u'Beautiful Garden Of Prayer (Demonstration)',
u'authors': [u'Eleanor Allen Schroll', u'James H. Fillmore'],
u'copyright': u'Public Domain ',
u'ccli_number': 60252,
u'verses':
[(u'There\'s a garden where Jesus is waiting,\r\nThere\'s a place that is wondrously fair.\r\n'
u'For it glows with the light of His presence,\r\n\'Tis the beautiful garden of prayer.', u'v1'),
(u'There\'s a garden where Jesus is waiting,\r\nAnd I go with my burden and care.\r\n'
u'Just to learn from His lips, words of comfort,\r\nIn the beautiful garden of prayer.', u'v2'),
(u'There\'s a garden where Jesus is waiting,\r\nAnd He bids you to come meet Him there,\r\n'
u'Just to bow and receive a new blessing,\r\nIn the beautiful garden of prayer.', u'v3'),
(u'O the beautiful garden, the garden of prayer,\r\nO the beautiful garden of prayer.\r\n'
u'There my Savior awaits, and He opens the gates\r\nTo the beautiful garden of prayer.', u'c1')],
u'topics': [u'Devotion', u'Prayer'],
u'comments': u'',
u'song_book_name': u'',
u'song_number': 0,
u'verse_order_list': []}}
class TestSongShowPlusImport(TestCase):
"""
Test the functions in the :mod:`songshowplusimport` module.
"""
def create_importer_test(self):
"""
Test creating an instance of the SongShow Plus file importer
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
mocked_manager = MagicMock()
# WHEN: An importer object is created
importer = SongShowPlusImport(mocked_manager)
# THEN: The importer object should not be None
self.assertIsNotNone(importer, u'Import should not be none')
def invalid_import_source_test(self):
"""
Test SongShowPlusImport.doImport handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
importer = SongShowPlusImport(mocked_manager)
importer.import_wizard = mocked_import_wizard
importer.stop_import_flag = True
# WHEN: Import source is not a list
for source in [u'not a list', 0]:
importer.import_source = source
# THEN: doImport should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.doImport(), u'doImport should return None when import_source is not a list')
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
u'setMaxium on import_wizard.progress_bar should not have been called')
def valid_import_source_test(self):
"""
Test SongShowPlusImport.doImport handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
importer = SongShowPlusImport(mocked_manager)
importer.import_wizard = mocked_import_wizard
importer.stop_import_flag = True
# WHEN: Import source is a list
importer.import_source = [u'List', u'of', u'files']
# THEN: doImport should return none and the progress bar setMaximum should be called with the length of
# import_source.
self.assertIsNone(importer.doImport(),
u'doImport should return None when import_source is a list and stop_import_flag is True')
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
def to_openlp_verse_tag_test(self):
"""
Test to_openlp_verse_tag method by simulating adding a verse
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
mocked_manager = MagicMock()
importer = SongShowPlusImport(mocked_manager)
# WHEN: Supplied with the following arguments replicating verses being added
test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
(u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
(u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
(u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
(u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
(u'different 1', VerseType.tags[VerseType.Other] + u'1'),
(u'random 1', VerseType.tags[VerseType.Other] + u'2')]
# THEN: The returned value should should correlate with the input arguments
for original_tag, openlp_tag in test_values:
self.assertEquals(importer.to_openlp_verse_tag(original_tag), openlp_tag,
u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
% (openlp_tag, original_tag))
def to_openlp_verse_tag_verse_order_test(self):
"""
Test to_openlp_verse_tag method by simulating adding a verse to the verse order
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
mocked_manager = MagicMock()
importer = SongShowPlusImport(mocked_manager)
# WHEN: Supplied with the following arguments replicating a verse order being added
test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
(u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
(u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
(u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
(u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
(u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
(u'different 1', VerseType.tags[VerseType.Other] + u'1'),
(u'random 1', VerseType.tags[VerseType.Other] + u'2'),
(u'unused 2', None)]
# THEN: The returned value should should correlate with the input arguments
for original_tag, openlp_tag in test_values:
self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag,
u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
% (openlp_tag, original_tag))
def file_import_test(self):
"""
Test the actual import of real song files and check that the imported data is correct.
"""
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
# and mocked out "author", "add_copyright", "add_verse", "finish" methods.
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
for song_file in SONG_TEST_DATA:
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
mocked_parse_author = MagicMock()
mocked_add_copyright = MagicMock()
mocked_add_verse = MagicMock()
mocked_finish = MagicMock()
mocked_finish.return_value = True
importer = SongShowPlusImport(mocked_manager)
importer.import_wizard = mocked_import_wizard
importer.stop_import_flag = False
importer.parse_author = mocked_parse_author
importer.addCopyright = mocked_add_copyright
importer.addVerse = mocked_add_verse
importer.finish = mocked_finish
importer.topics = []
# WHEN: Importing each file
importer.import_source = [os.path.join(TEST_PATH, song_file)]
title = SONG_TEST_DATA[song_file][u'title']
author_calls = SONG_TEST_DATA[song_file][u'authors']
song_copyright = SONG_TEST_DATA[song_file][u'copyright']
ccli_number = SONG_TEST_DATA[song_file][u'ccli_number']
add_verse_calls = SONG_TEST_DATA[song_file][u'verses']
topics = SONG_TEST_DATA[song_file][u'topics']
comments = SONG_TEST_DATA[song_file][u'comments']
song_book_name = SONG_TEST_DATA[song_file][u'song_book_name']
song_number = SONG_TEST_DATA[song_file][u'song_number']
verse_order_list = SONG_TEST_DATA[song_file][u'verse_order_list']
# THEN: doImport should return none, the song data should be as expected, and finish should have been
# called.
self.assertIsNone(importer.doImport(), u'doImport should return None when it has completed')
self.assertEquals(importer.title, title, u'title for %s should be "%s"' % (song_file, title))
for author in author_calls:
mocked_parse_author.assert_any_call(author)
if song_copyright:
mocked_add_copyright.assert_called_with(song_copyright)
if ccli_number:
self.assertEquals(importer.ccliNumber, ccli_number, u'ccliNumber for %s should be %s'
% (song_file, ccli_number))
for verse_text, verse_tag in add_verse_calls:
mocked_add_verse.assert_any_call(verse_text, verse_tag)
if topics:
self.assertEquals(importer.topics, topics, u'topics for %s should be %s' % (song_file, topics))
if comments:
self.assertEquals(importer.comments, comments, u'comments for %s should be "%s"'
% (song_file, comments))
if song_book_name:
self.assertEquals(importer.songBookName, song_book_name, u'songBookName for %s should be "%s"'
% (song_file, song_book_name))
if song_number:
self.assertEquals(importer.songNumber, song_number, u'songNumber for %s should be %s'
% (song_file, song_number))
if verse_order_list:
self.assertEquals(importer.verseOrderList, [], u'verseOrderList for %s should be %s'
% (song_file, verse_order_list))
mocked_finish.assert_called_with()

View File

@ -36,19 +36,29 @@ class TestEditCustomForm(TestCase):
del self.main_window
del self.app
def load_themes_test(self):
"""
Test the load_themes() method.
"""
# GIVEN: A theme list.
theme_list = [u'First Theme', u'Second Theme']
# WHEN: Show the dialog and add pass a theme list.
self.form.load_themes(theme_list)
# THEN: There should be three items in the combo box.
assert self.form.theme_combo_box.count() == 3, u'There should be three items (themes) in the combo box.'
def load_custom_test(self):
"""
Test the load_custom() method.
"""
# GIVEN: A mocked QDialog.exec_() method
with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec:
# WHEN: Show the dialog and create a new custom item.
self.form.exec_()
self.form.load_custom(0)
# WHEN: Create a new custom item.
self.form.load_custom(0)
#THEN: The line edits should not contain any text.
self.assertEqual(self.form.title_edit.text(), u'', u'The title edit should be empty')
self.assertEqual(self.form.credit_edit.text(), u'', u'The credit edit should be empty')
# THEN: The line edits should not contain any text.
self.assertEqual(self.form.title_edit.text(), u'', u'The title edit should be empty')
self.assertEqual(self.form.credit_edit.text(), u'', u'The credit edit should be empty')
def on_add_button_clicked_test(self):
@ -57,10 +67,10 @@ class TestEditCustomForm(TestCase):
"""
# GIVEN: A mocked QDialog.exec_() method
with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec:
# WHEN: Show the dialog and add a new slide.
self.form.exec_()
# WHEN: Add a new slide.
QtTest.QTest.mouseClick(self.form.add_button, QtCore.Qt.LeftButton)
#THEN: One slide should be added.
# THEN: One slide should be added.
assert self.form.slide_list_view.count() == 1, u'There should be one slide added.'
def validate_not_valid_part1_test(self):

View File

@ -0,0 +1,138 @@
"""
This module contains tests for the lib submodule of the Remotes plugin.
"""
import os
from unittest import TestCase
from tempfile import mkstemp
from mock import MagicMock
import urllib2
import cherrypy
from BeautifulSoup import BeautifulSoup
from openlp.core.lib import Settings
from openlp.plugins.remotes.lib.httpserver import HttpServer
from PyQt4 import QtGui
__default_settings__ = {
u'remotes/twelve hour': True,
u'remotes/port': 4316,
u'remotes/https port': 4317,
u'remotes/https enabled': False,
u'remotes/user id': u'openlp',
u'remotes/password': u'password',
u'remotes/authentication enabled': False,
u'remotes/ip address': u'0.0.0.0'
}
class TestRouter(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Create the UI
"""
fd, self.ini_file = mkstemp(u'.ini')
Settings().set_filename(self.ini_file)
self.application = QtGui.QApplication.instance()
Settings().extend_default_settings(__default_settings__)
self.server = HttpServer()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.application
os.unlink(self.ini_file)
self.server.close()
def start_server(self):
"""
Common function to start server then mock out the router. CherryPy crashes if you mock before you start
"""
self.server.start_server()
self.server.router = MagicMock()
self.server.router.process_http_request = process_http_request
def start_default_server_test(self):
"""
Test the default server serves the correct initial page
"""
# GIVEN: A default configuration
Settings().setValue(u'remotes/authentication enabled', False)
self.start_server()
# WHEN: called the route location
code, page = call_remote_server(u'http://localhost:4316')
# THEN: default title will be returned
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
u'The default menu should be returned')
def start_authenticating_server_test(self):
"""
Test the default server serves the correctly with authentication
"""
# GIVEN: A default authorised configuration
Settings().setValue(u'remotes/authentication enabled', True)
self.start_server()
# WHEN: called the route location with no user details
code, page = call_remote_server(u'http://localhost:4316')
# THEN: then server will ask for details
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
# WHEN: called the route location with user details
code, page = call_remote_server(u'http://localhost:4316', u'openlp', u'password')
# THEN: default title will be returned
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
u'The default menu should be returned')
# WHEN: called the route location with incorrect user details
code, page = call_remote_server(u'http://localhost:4316', u'itwinkle', u'password')
# THEN: then server will ask for details
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
def call_remote_server(url, username=None, password=None):
"""
Helper function
``username``
The username.
``password``
The password.
"""
if username:
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
passman.add_password(None, url, username, password)
authhandler = urllib2.HTTPBasicAuthHandler(passman)
opener = urllib2.build_opener(authhandler)
urllib2.install_opener(opener)
try:
page = urllib2.urlopen(url)
return 0, page.read()
except urllib2.HTTPError, e:
return e.code, u''
def process_http_request(url_path, *args):
"""
Override function to make the Mock work but does nothing.
``Url_path``
The url_path.
``*args``
Some args.
"""
cherrypy.response.status = 200
return None

View File

View File

Binary file not shown.