diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 95a8122d6..23986ffc4 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -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() diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index f6c95aa9e..01329b842 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -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. diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index dd9843930..b4f851b24 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -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. diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 8fc294ea6..db96e3fa7 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -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): """ diff --git a/openlp/core/lib/screen.py b/openlp/core/lib/screen.py index 368035e17..84e7e4258 100644 --- a/openlp/core/lib/screen.py +++ b/openlp/core/lib/screen.py @@ -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') diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index f57243818..c4ac846c9 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -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 diff --git a/openlp/core/lib/settings.py b/openlp/core/lib/settings.py index 30a8b25d8..49cd8f6d5 100644 --- a/openlp/core/lib/settings.py +++ b/openlp/core/lib/settings.py @@ -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 diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index f7dc83d29..9f8cf8093 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -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': diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index e828db597..0f3f3cc18 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -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: diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 58752a4a9..49497a10e 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -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): """ diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 9b4037875..2504520c0 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -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(): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 69439c1b1..642f1543b 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -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() diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 51a8f3517..1e011a84d 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -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): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 6e0f4ad95..f12d55207 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -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)) diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index eeb85fa66..bc40539cf 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -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 diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index f0c5aa170..35527e1e4 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -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) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 89bbe86f8..be0e3bfa1 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -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 diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index d32729699..d4ee8039c 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -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'] diff --git a/openlp/core/utils/languagemanager.py b/openlp/core/utils/languagemanager.py index 00a0d0079..6dc18c1ad 100644 --- a/openlp/core/utils/languagemanager.py +++ b/openlp/core/utils/languagemanager.py @@ -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, diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 042999a11..830ad05c9 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -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): """ diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index e360cd4a1..f8d771e77 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -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): diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 44b19f857..da79985e3 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -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. diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index abe3cc45a..86a507612 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -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): diff --git a/openlp/plugins/custom/lib/db.py b/openlp/plugins/custom/lib/db.py index cc6e45742..253ca5432 100644 --- a/openlp/plugins/custom/lib/db.py +++ b/openlp/plugins/custom/lib/db.py @@ -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): diff --git a/openlp/plugins/images/__init__.py b/openlp/plugins/images/__init__.py index 12e0cc9e4..9830af231 100644 --- a/openlp/plugins/images/__init__.py +++ b/openlp/plugins/images/__init__.py @@ -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. """ diff --git a/openlp/plugins/images/forms/__init__.py b/openlp/plugins/images/forms/__init__.py index d308a1471..8bb8e966f 100644 --- a/openlp/plugins/images/forms/__init__.py +++ b/openlp/plugins/images/forms/__init__.py @@ -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_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_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 -``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 ``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 diff --git a/openlp/plugins/images/forms/addgroupform.py b/openlp/plugins/images/forms/addgroupform.py index 7f7986499..4cdc6a73b 100644 --- a/openlp/plugins/images/forms/addgroupform.py +++ b/openlp/plugins/images/forms/addgroupform.py @@ -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', diff --git a/openlp/plugins/images/forms/choosegroupform.py b/openlp/plugins/images/forms/choosegroupform.py index bbb57255c..f11c8324c 100644 --- a/openlp/plugins/images/forms/choosegroupform.py +++ b/openlp/plugins/images/forms/choosegroupform.py @@ -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) diff --git a/openlp/plugins/images/imageplugin.py b/openlp/plugins/images/imageplugin.py index cb25dc375..dfe927a7b 100644 --- a/openlp/plugins/images/imageplugin.py +++ b/openlp/plugins/images/imageplugin.py @@ -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')) diff --git a/openlp/plugins/images/lib/db.py b/openlp/plugins/images/lib/db.py index bc4a94f15..1d8f473d8 100644 --- a/openlp/plugins/images/lib/db.py +++ b/openlp/plugins/images/lib/db.py @@ -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 diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index d74b1ccab..95f0971fd 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -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.')) diff --git a/openlp/plugins/media/__init__.py b/openlp/plugins/media/__init__.py index deb4cbeac..2a2e9f5aa 100644 --- a/openlp/plugins/media/__init__.py +++ b/openlp/plugins/media/__init__.py @@ -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. """ diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 57bc6947b..2037346ad 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -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): diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 790dce03c..38cd9bb69 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -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() diff --git a/openlp/plugins/presentations/__init__.py b/openlp/plugins/presentations/__init__.py index f147ea524..d600d3793 100644 --- a/openlp/plugins/presentations/__init__.py +++ b/openlp/plugins/presentations/__init__.py @@ -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. """ diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index d4c933d33..d30c71078 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -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 diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index f92562541..2f48b99c1 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -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() diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index d87e7e5dc..330c36f5c 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -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() diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 7a9f548ee..6895fda19 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -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 diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index a2dc56f52..abb9fd11e 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -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) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 48955ebb2..7501fd6df 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -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: diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index cecec53b5..e46467403 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -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 diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 7872c25b7..1cb966aa5 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -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', 'Presentation ' 'Plugin
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] = { diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 00877e332..3cbe65366 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -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; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index dcc2e4b70..dff51537c 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -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 || diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index d285baa50..878b197b3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -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 = ["Sorry, an error occurred "] 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) diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 483b3461b..09934b58c 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -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 download 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'%s' % (url, url)) - url += u'stage' - self.stage_url.setText(u'%s' % (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'%s' % (http_url, http_url)) + self.remote_https_url.setText(u'%s' % (https_url, https_url)) + http_url += u'stage' + https_url += u'stage' + self.stage_url.setText(u'%s' % (http_url, http_url)) + self.stage_https_url.setText(u'%s' % (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()) + diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index e990101e3..f443fbda4 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -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.')) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 49a20762a..fcc7f4f21 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -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): diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 79f21a454..f0554f588 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -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: diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 5c1485b9e..d3005c9b2 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -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 diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index db5f59357..015caa87d 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -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): diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 0c4898fd9..1565169e2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -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) ] diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index c5a63333c..8886e2884 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -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): """ diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index aadc61719..a72f83c4f 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -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 diff --git a/openlp/plugins/songusage/__init__.py b/openlp/plugins/songusage/__init__.py index d18c787f0..b0d3ecc12 100644 --- a/openlp/plugins/songusage/__init__.py +++ b/openlp/plugins/songusage/__init__.py @@ -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. """ diff --git a/openlp/plugins/songusage/forms/songusagedeletedialog.py b/openlp/plugins/songusage/forms/songusagedeletedialog.py index a1ad701b2..cffbdf733 100644 --- a/openlp/plugins/songusage/forms/songusagedeletedialog.py +++ b/openlp/plugins/songusage/forms/songusagedeletedialog.py @@ -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.')) diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index 47fc9bf27..1922d261a 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -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')) diff --git a/openlp/plugins/songusage/lib/db.py b/openlp/plugins/songusage/lib/db.py index 048b75542..5d3da7559 100644 --- a/openlp/plugins/songusage/lib/db.py +++ b/openlp/plugins/songusage/lib/db.py @@ -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 diff --git a/openlp/plugins/songusage/songusageplugin.py b/openlp/plugins/songusage/songusageplugin.py index 7ca056184..7a730c992 100644 --- a/openlp/plugins/songusage/songusageplugin.py +++ b/openlp/plugins/songusage/songusageplugin.py @@ -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', 'SongUsage Plugin' - '
This plugin tracks the usage of songs in services.') + about_text = translate('SongUsagePlugin', + 'SongUsage Plugin
This plugin tracks the usage of songs in services.') return about_text def set_plugin_text_strings(self): diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 86a029b71..222441f95 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -81,8 +81,10 @@ MODULES = [ 'enchant', 'bs4', 'mako', + 'cherrypy', 'migrate', 'uno', + 'icu', ] diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 66cb834f1..c03a11265 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -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 diff --git a/tests/functional/openlp_core_lib/test_pluginmanager.py b/tests/functional/openlp_core_lib/test_pluginmanager.py index 9d6c30f8e..8317e78dc 100644 --- a/tests/functional/openlp_core_lib/test_pluginmanager.py +++ b/tests/functional/openlp_core_lib/test_pluginmanager.py @@ -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): """ diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index d50ddc978..26e9e7d44 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -276,5 +276,7 @@ class TestServiceItem(TestCase): first_line = items[0] except IOError: first_line = u'' + finally: + open_file.close() return first_line diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 827bfa156..786a884a0 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -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): """ diff --git a/tests/functional/openlp_core_lib/test_uistrings.py b/tests/functional/openlp_core_lib/test_uistrings.py index 3351657d1..0070533db 100644 --- a/tests/functional/openlp_core_lib/test_uistrings.py +++ b/tests/functional/openlp_core_lib/test_uistrings.py @@ -6,6 +6,7 @@ from unittest import TestCase from openlp.core.lib import UiStrings + class TestUiStrings(TestCase): def check_same_instance_test(self): diff --git a/tests/functional/openlp_core_utils/test_applocation.py b/tests/functional/openlp_core_utils/test_applocation.py index 5473da8c0..b59f41f37 100644 --- a/tests/functional/openlp_core_utils/test_applocation.py +++ b/tests/functional/openlp_core_utils/test_applocation.py @@ -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"' diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 2e826bc61..8e3a427ed 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -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' diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index a355e956b..5033f0645 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -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' diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py new file mode 100644 index 000000000..22bee8139 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -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') diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py new file mode 100644 index 000000000..2980a339b --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -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.' diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py new file mode 100644 index 000000000..86d77bbdc --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -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() diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index dbed244e3..41671403b 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -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): diff --git a/tests/interfaces/openlp_plugins/remotes/__init__.py b/tests/interfaces/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py new file mode 100644 index 000000000..8795eeaf3 --- /dev/null +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -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 + diff --git a/tests/resources/remotes/openlp.crt b/tests/resources/remotes/openlp.crt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/remotes/openlp.key b/tests/resources/remotes/openlp.key new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/songshowplussongs/Amazing Grace.sbsong b/tests/resources/songshowplussongs/Amazing Grace.sbsong new file mode 100644 index 000000000..14b7c3597 Binary files /dev/null and b/tests/resources/songshowplussongs/Amazing Grace.sbsong differ diff --git a/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong new file mode 100644 index 000000000..c227d4809 Binary files /dev/null and b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong differ